#!/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 # Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in # die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt # LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel # nicht erzeugt, warnt **jede neu gestartete Shell**: # # bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8) # # Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist # keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier # laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders # klingt, liest sich schlecht. if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then export LC_ALL=C.UTF-8 LANG=C.UTF-8 else export LC_ALL=C LANG=C fi 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