diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index 481ecf0..672c7ec 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.8.7" +version = "2.8.8" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh new file mode 100644 index 0000000..3dcc7f5 --- /dev/null +++ b/deploy/uninstall.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash +# ============================================================================= +# Rueckbau einer Installation von TESM oder TESM-Lizenzserver. +# +# Aufruf: +# sudo bash deploy/uninstall.sh --app tesm +# sudo bash deploy/uninstall.sh --app tesm-license --instance test +# sudo bash deploy/uninstall.sh --app tesm --dry-run +# +# Das genaue Gegenstueck zu install.sh -- und es fehlte. Wer eine Installation +# von Hand loeschte ("rm -rf /srv/tesm-probe"), liess zurueck: die systemd-Unit +# (die systemd danach in einer Dauerschleife neu zu starten versuchte), den +# Begruessungshaken (der bei jeder Anmeldung seinen Banner druckte), den +# Shim unter /usr/local/bin, die Umgebungsdatei, die nginx-Site und den +# Dienstbenutzer. Auf dem Testserver standen so neun tote Dienste und vierzehn +# Banner. Genau das raeumt dieses Skript ab. +# +# Zwei Regeln, die den Unterschied machen: +# +# * **Daten bleiben.** Datenbank, Schluessel und Lizenz werden vorher in ein +# Archiv gelegt, nicht geloescht. Wer sie wirklich los sein will, sagt es +# mit --purge. Ein Rueckbau, der beilaeufig die Datenbank mitnimmt, ist +# keiner, sondern ein Unfall. +# * **Geteiltes bleibt, solange es jemand braucht.** sudoers-Datei und +# Dienstbenutzer gehoeren der *Anwendung* (nicht der Instanz), der Helfer +# unter /usr/local/lib/tesm allen Installationen beider Anwendungen. Sie +# verschwinden erst, wenn die letzte geht -- sonst legt der Rueckbau einer +# Testinstanz die Produktion daneben lahm. +# ============================================================================= +set -euo pipefail + +APP="" +INSTANCE="" +PURGE=0 +ASSUME_YES=0 +DRY_RUN=0 + +#: Ueberschreibbar, damit sich die Entscheidungen dieses Skripts in einem Test +#: wirklich ausfuehren lassen -- und nicht nur als Textmuster geprueft werden. +ETC_TESM="${TESM_ETC_DIR:-/etc/tesm}" +SRV_DIR="${TESM_SRV_DIR:-/srv}" +UNIT_DIR="${TESM_SYSTEMD_DIR:-/etc/systemd/system}" +MOTD_DIR="${TESM_MOTD_DIR:-/etc/update-motd.d}" +BIN_DIR="${TESM_BIN_DIR:-/usr/local/bin}" +LIB_DIR="${TESM_LIB_DIR:-/usr/local/lib/tesm}" +SUDOERS_DIR="${TESM_SUDOERS_DIR:-/etc/sudoers.d}" +LOGROTATE_DIR="${TESM_LOGROTATE_DIR:-/etc/logrotate.d}" +NGINX_DIR="${TESM_NGINX_DIR:-/etc/nginx}" +LOG_BASE="${TESM_LOG_BASE:-/var/log}" + +COLOR_OK=$'\033[32m'; COLOR_WARN=$'\033[33m'; COLOR_ERR=$'\033[31m'; COLOR_OFF=$'\033[0m' +step() { printf '\n%s==>%s %s\n' "$COLOR_OK" "$COLOR_OFF" "$*"; } +warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*" ; } +fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; } +info() { printf ' %s\n' "$*"; } + +usage() { + cat <<'TEXT' +Rueckbau einer Installation. + + --app welche Anwendung (Pflicht) + --instance benannte Instanz (sonst die Hauptinstallation) + --purge Daten nicht sichern, sondern loeschen + --dry-run nur zeigen, was geschehen wuerde + --yes, -y nicht nachfragen +TEXT +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --app) APP="${2:?}"; shift 2 ;; + --instance) INSTANCE="${2:?}"; shift 2 ;; + --purge) PURGE=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + --yes|-y) ASSUME_YES=1; shift ;; + -h|--help) usage; exit 0 ;; + *) fail "unbekannte Option: $1" ;; + esac +done + +[[ -n "$APP" ]] || { usage; fail "--app fehlt"; } +[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || fail "--app muss tesm oder tesm-license sein" +[[ -z "$INSTANCE" || "$INSTANCE" =~ ^[a-z0-9-]{1,16}$ ]] || fail "ungueltiger Instanzname" +# Root braucht es wegen /srv, /etc und systemd. Wer die Pfade umbiegt (Test), +# fasst nichts davon an -- dann ist die Huerde keine Sicherheit, sondern nur im +# Weg, und ein Rueckbau, der sich nicht pruefen laesst, ist ein Risiko fuer sich. +if [[ $DRY_RUN -eq 0 && "$SRV_DIR" == "/srv" && "$(id -u)" -ne 0 ]]; then + fail "bitte mit sudo ausfuehren" +fi + +NAME="$APP" +[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE" +ROOT="$SRV_DIR/$NAME" + +# Das Datenverzeichnis kann verschoben sein -- dann steht es in der +# Umgebungsdatei, und nur dort. Ein Rueckbau, der stattdessen "$ROOT/data" +# annimmt, sichert ein leeres Verzeichnis und laesst die echte Datenbank liegen. +ENV_FILE="$ETC_TESM/$NAME.env" +DATEN_DIR="$(sed -n 's/^TESM_DATA_DIR=//p' "$ENV_FILE" 2>/dev/null | tail -1 || true)" +DATEN_DIR="${DATEN_DIR:-$ROOT/data}" + +# --------------------------------------------------------------------------- # +# Wer teilt sich was? +# --------------------------------------------------------------------------- # + +#: Gibt es neben dieser noch eine weitere Installation **derselben** Anwendung? +#: Daran haengen sudoers-Datei und Dienstbenutzer -- beide tragen den Namen der +#: Anwendung, nicht den der Instanz. +weitere_dieser_anwendung() { + local eintrag name + for eintrag in "$ETC_TESM/$APP.env" "$ETC_TESM/$APP-"*.env; do + [[ -f "$eintrag" ]] || continue + name="$(basename "$eintrag" .env)" + [[ "$name" == "$NAME" ]] && continue + return 0 + done + return 1 +} + +#: Gibt es ueberhaupt noch irgendeine Installation? Daran haengen der Helfer +#: und das Banner-Programm unter /usr/local/lib/tesm -- die gehoeren allen. +weitere_ueberhaupt() { + local eintrag name + for eintrag in "$ETC_TESM"/*.env; do + [[ -f "$eintrag" ]] || continue + name="$(basename "$eintrag" .env)" + [[ "$name" == "$NAME" ]] && continue + return 0 + done + return 1 +} + +# --------------------------------------------------------------------------- # +# Der Plan -- erst zeigen, dann tun +# --------------------------------------------------------------------------- # +UNITS=("$NAME.service" "$NAME-monitor.service" "$NAME-cleanup.service" "$NAME-cleanup.timer") +DATEIEN=( + "$NGINX_DIR/sites-enabled/$NAME" + "$NGINX_DIR/sites-available/$NAME" + "$MOTD_DIR/50-$NAME" + "$BIN_DIR/$NAME-admin" + "$LOGROTATE_DIR/$NAME" + "$ENV_FILE" +) +VERZEICHNISSE=("$ETC_TESM/certs/$NAME" "$LOG_BASE/$NAME") + +gefunden=0 +step "Rueckbau von $NAME" +info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}" +info "Verzeichnis : $ROOT" + +echo +echo " Dienste:" +for unit in "${UNITS[@]}"; do + if [[ -f "$UNIT_DIR/$unit" ]]; then info "- $unit"; gefunden=1; fi +done +[[ $gefunden -eq 1 ]] || info "(keine)" + +echo +echo " Dateien und Verzeichnisse:" +for pfad in "${DATEIEN[@]}" "${VERZEICHNISSE[@]}"; do + if [[ -e "$pfad" ]]; then info "- $pfad"; gefunden=1; fi +done +if [[ -d "$ROOT" ]]; then info "- $ROOT"; gefunden=1; fi + +if [[ $gefunden -eq 0 ]]; then + echo + info "Von '$NAME' ist nichts mehr da -- nichts zu tun." + exit 0 +fi + +echo +echo " Daten (Datenbank, Schluessel, Lizenz):" +if [[ -d "$DATEN_DIR" ]]; then + if [[ $PURGE -eq 1 ]]; then + warn "$DATEN_DIR wird GELOESCHT (--purge)" + else + info "$DATEN_DIR wird vorher gesichert" + fi +else + info "(kein Datenverzeichnis gefunden)" +fi + +echo +echo " Bleibt stehen:" +if weitere_dieser_anwendung; then + info "sudoers und Dienstbenutzer '$APP' -- eine weitere Installation braucht sie" +else + info "- (sudoers und Dienstbenutzer '$APP' werden entfernt)" +fi +if weitere_ueberhaupt; then + info "Helfer unter $LIB_DIR -- andere Installationen brauchen ihn" +else + info "- (Helfer unter $LIB_DIR wird entfernt)" +fi + +if [[ $DRY_RUN -eq 1 ]]; then + echo + info "Probelauf -- es wurde nichts geaendert." + exit 0 +fi + +if [[ $ASSUME_YES -eq 0 ]]; then + echo + read -r -p "Fortfahren? [j/N] " antwort + [[ "$antwort" =~ ^[jJyY]$ ]] || fail "abgebrochen" +fi + +# --------------------------------------------------------------------------- # +# Ausfuehren +# --------------------------------------------------------------------------- # +step "Daten" +if [[ -d "$DATEN_DIR" && $PURGE -eq 0 ]]; then + ARCHIV="$SRV_DIR/$NAME-daten-$(date +%Y%m%d-%H%M%S).tar.gz" + tar -czf "$ARCHIV" -C "$(dirname "$DATEN_DIR")" "$(basename "$DATEN_DIR")" + chmod 600 "$ARCHIV" + info "gesichert: $ARCHIV" +elif [[ -d "$DATEN_DIR" ]]; then + info "wird mit dem Verzeichnis geloescht (--purge)" +else + info "nichts zu sichern" +fi + +step "Dienste" +for unit in "${UNITS[@]}"; do + [[ -f "$UNIT_DIR/$unit" ]] || continue + systemctl disable --now "$unit" >/dev/null 2>&1 || true + rm -f "$UNIT_DIR/$unit" + # Ohne das bleibt eine entfernte Unit als "not-found" im Fehlerzustand + # stehen und faerbt "systemctl --failed" rot. + systemctl reset-failed "$unit" >/dev/null 2>&1 || true + info "entfernt: $unit" +done +systemctl daemon-reload >/dev/null 2>&1 || true + +step "Webserver" +if [[ -e "$NGINX_DIR/sites-enabled/$NAME" || -e "$NGINX_DIR/sites-available/$NAME" ]]; then + rm -f "$NGINX_DIR/sites-enabled/$NAME" "$NGINX_DIR/sites-available/$NAME" + # Drei Faelle, und sie bedeuten Verschiedenes: kein nginx da (dann gibt es + # nichts neu zu laden), nginx da und zufrieden, nginx da und unzufrieden. + # Sie in einen Zweig zu werfen hiesse, auf einer Maschine ohne nginx eine + # Warnung ueber eine kaputte Konfiguration auszugeben, die es nicht gibt. + if ! command -v nginx >/dev/null 2>&1; then + info "Site entfernt (nginx ist hier nicht installiert)" + elif nginx -t >/dev/null 2>&1; then + systemctl reload nginx >/dev/null 2>&1 || true + info "Site entfernt, nginx neu geladen" + else + warn "Site entfernt -- nginx beanstandet die Konfiguration, bitte 'nginx -t' ansehen" + fi +else + info "keine Site vorhanden" +fi + +step "Dateien" +for pfad in "${DATEIEN[@]}"; do + [[ -e "$pfad" ]] && { rm -f "$pfad"; info "entfernt: $pfad"; } +done +for pfad in "${VERZEICHNISSE[@]}"; do + [[ -d "$pfad" ]] && { rm -rf "$pfad"; info "entfernt: $pfad"; } +done +if [[ -d "$ROOT" ]]; then + rm -rf "$ROOT" + info "entfernt: $ROOT" +fi +if [[ $PURGE -eq 1 ]]; then + # Die Rueckwege aelterer Aktualisierungen. Sie enthalten dieselben Daten wie + # die Installation -- deshalb nur hier, nie im Normalfall. + for rest in "$SRV_DIR/$NAME-backup-"* "$SRV_DIR/$NAME-failed-"*; do + [[ -d "$rest" ]] && { rm -rf "$rest"; info "entfernt: $rest"; } + done +fi + +step "Geteiltes" +if weitere_dieser_anwendung; then + info "sudoers und Dienstbenutzer '$APP' bleiben -- eine weitere Installation braucht sie" +else + rm -f "$SUDOERS_DIR/$APP" + info "entfernt: $SUDOERS_DIR/$APP" + if getent passwd "$APP" >/dev/null 2>&1; then + # Ein laufender Prozess haelt den Benutzer fest. Das ist kein Grund, + # den Rueckbau scheitern zu lassen -- der Dienst ist oben schon gestoppt, + # und was sonst noch unter diesem Namen laeuft, gehoert angesehen. + if userdel "$APP" >/dev/null 2>&1; then + info "entfernt: Dienstbenutzer $APP" + else + warn "Dienstbenutzer $APP liess sich nicht entfernen -- laeuft noch etwas unter ihm?" + fi + fi +fi +if weitere_ueberhaupt; then + info "Helfer unter $LIB_DIR bleibt -- andere Installationen brauchen ihn" +else + rm -rf "$LIB_DIR" + rmdir "$ETC_TESM/certs" "$ETC_TESM" >/dev/null 2>&1 || true + info "entfernt: $LIB_DIR" +fi + +step "Fertig" +info "'$NAME' ist zurueckgebaut." +if [[ $PURGE -eq 0 && -n "${ARCHIV:-}" ]]; then + info "Die Daten liegen unter $ARCHIV -- dort und sonst nirgends." +fi diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 8cdcd22..b32c1ab 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -383,6 +383,44 @@ sudo bash /srv/tesm/deploy/update.sh tesm --- +## 4c. Installation entfernen + +Das Gegenstück zu `install.sh`: + +```bash +sudo bash /srv/tesm/deploy/uninstall.sh --app tesm --dry-run +sudo bash /srv/tesm/deploy/uninstall.sh --app tesm +sudo bash /srv/tesm/deploy/uninstall.sh --app tesm-license --instance test --yes +``` + +Entfernt werden Dienste (`.service`, `-monitor`, `-cleanup` samt Timer), die +nginx-Site, der Begrüßungshaken, der Shim unter `/usr/local/bin`, die +Umgebungsdatei, das Zertifikatsverzeichnis, das Protokollverzeichnis und die +Installation selbst. + +**Die Daten bleiben.** Datenbank, Schlüssel und Lizenz gehen vorher in ein +Archiv (`/srv/-daten-.tar.gz`, nur root). Sie sind das einzige an +einer Installation, was sich nicht wiederherstellen lässt. Wer sie wirklich +los sein will, nimmt `--purge` — das löscht zusätzlich die Rückwege älterer +Aktualisierungen (`/srv/-backup-*`). + +**Geteiltes bleibt, solange es jemand braucht.** Die sudoers-Datei und der +Dienstbenutzer tragen den Namen der *Anwendung*, nicht den der Instanz; der +Helfer unter `/usr/local/lib/tesm` gehört allen Installationen beider +Anwendungen. Sie verschwinden erst mit der letzten. Sonst legte der Rückbau +einer Testinstanz die Produktion daneben lahm. + +`--dry-run` zeigt den Plan, ohne etwas zu ändern. Ein zweiter Lauf auf eine +bereits entfernte Installation sagt das und endet mit 0. + +Warum es das gibt: wer eine Installation von Hand löscht (`rm -rf /srv/…`), +lässt Unit, Begrüßungshaken, Shim, sudoers-Datei und nginx-Site zurück. +systemd versucht die Unit danach in einer Dauerschleife zu starten, und der +Haken druckt bei jeder Anmeldung seinen Banner. Auf dem Testserver standen so +neun tote Dienste und vierzehn Banner. + +--- + ## 5. Dienste ```bash