#!/usr/bin/env bash # ============================================================================= # Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation # (oder umgekehrt). # # sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm # sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test \ # --to tesm-license # # Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses # erwartet, und drei davon sind leicht zu vergessen: # # * ``SITE_KEY`` in der Umgebungsdatei -- daraus entsteht der Name des # Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte # Instanz. # * ``web_cert_path``/``web_key_path``/``web_acme_webroot`` in der Datenbank. # Sie enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx # nach dem Umzug nicht mehr. # * ``server_name`` der nginx-Site. install.sh schreibt die Datei neu, sobald # sich die alias-Pfade aendern -- und die aendern sich hier zwangslaeufig. # Ohne den Namen aus der Datenbank stuenden danach zwei Sites mit ``_`` auf # Port 80 und die erste gewinnt. # # Belegt der Zielname noch eine Installation der Fassung 1.x, wird sie beiseite # geschoben: Verzeichnis nach ``-``, ihre systemd-Units und ihre # nginx-Site als Kopie hinein. Danach ist der Name frei und nichts auf dem # System zeigt mehr ins Leere. # # Alle angemeldeten Benutzer werden abgemeldet: das Sitzungscookie heisst # danach anders. # ============================================================================= set -euo pipefail APP="" FROM="" TO="" SUFFIX="sonnet5" ASSUME_YES=0 DRY_RUN=0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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 <<'USAGE' Verwendung: rename.sh --app --from --to [Optionen] --app Welche Anwendung (Pflicht) --from Bisheriger Installationsname, z.B. tesm-test (Pflicht) --to Neuer Installationsname, z.B. tesm (Pflicht) --suffix Namenszusatz fuer eine beiseite geschobene Installation der Fassung 1.x (Vorgabe: sonnet5) --dry-run Nur zeigen, was geschehen wuerde --yes Keine Rueckfrage USAGE } while [[ $# -gt 0 ]]; do case "$1" in --app) APP="${2:?}"; shift 2 ;; --from) FROM="${2:?}"; shift 2 ;; --to) TO="${2:?}"; shift 2 ;; --suffix) SUFFIX="${2:?}"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; --yes|-y) ASSUME_YES=1; shift ;; -h|--help) usage; exit 0 ;; *) usage; fail "unbekannte Option: $1" ;; esac done [[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; } [[ -n "$FROM" && -n "$TO" ]] || { usage; fail "--from und --to sind Pflicht"; } [[ "$FROM" != "$TO" ]] || fail "--from und --to sind gleich" [[ "$TO" =~ ^[a-z0-9-]{1,32}$ ]] || fail "ungueltiger Zielname: $TO" [[ "$TO" == "$APP" || "$TO" == "$APP-"* ]] || fail "der Zielname muss mit '$APP' beginnen" [[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren" [[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript" [[ -d "/srv/$FROM" ]] || fail "/srv/$FROM gibt es nicht" [[ -f "/srv/$FROM/data/app.db" ]] || fail "/srv/$FROM sieht nicht nach einer Installation dieser Fassung aus" ENV_PREFIX="TESM_" [[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_" # Alles, was den Namen traegt. Reihenfolge: erst Daten, dann Konfiguration. VERSCHIEBEN=( "/srv/$FROM:/srv/$TO" "/var/log/$FROM:/var/log/$TO" "/etc/tesm/$FROM.env:/etc/tesm/$TO.env" "/var/www/$FROM-acme:/var/www/$TO-acme" "/etc/tesm/certs/$FROM:/etc/tesm/certs/$TO" ) ENTFERNEN=( "/etc/systemd/system/$FROM.service" "/etc/systemd/system/$FROM-monitor.service" "/etc/nginx/sites-enabled/$FROM" "/etc/nginx/sites-available/$FROM" "/usr/local/bin/$FROM-admin" "/etc/logrotate.d/$FROM" "/etc/update-motd.d/50-$FROM" ) # -- Webeinstellungen auslesen, bevor irgendetwas umzieht -------------------- # # Mit dem System-Python und der Standardbibliothek: das venv zieht gleich mit # um, und ein zusaetzliches Werkzeug (sqlite3) ist auf einem schlanken Server # nicht garantiert. lese_einstellung() { python3 - "$1" "/srv/$FROM/data/app.db" <<'PY' import sqlite3, sys schluessel, datenbank = sys.argv[1], sys.argv[2] try: conn = sqlite3.connect(f"file:{datenbank}?mode=ro", uri=True) row = conn.execute("SELECT value FROM settings WHERE key=?", (schluessel,)).fetchone() conn.close() except sqlite3.Error: row = None print((row[0] if row and row[0] is not None else "").strip()) PY } DOMAIN="$(lese_einstellung web_domain || true)" HTTPS_AN="$(lese_einstellung web_https_enabled || true)" HSTS_AN="$(lese_einstellung web_hsts_enabled || true)" CERT_ALT="$(lese_einstellung web_cert_path || true)" step "Vorhaben" info "Anwendung : $APP" info "Alt : $FROM" info "Neu : $TO" info "Domain : ${DOMAIN:-(keine)}" info "HTTPS : $([[ "$HTTPS_AN" == "1" ]] && echo "ja${CERT_ALT:+, Zertifikat $CERT_ALT}" || echo nein)" for paar in "${VERSCHIEBEN[@]}"; do quelle="${paar%%:*}"; ziel="${paar##*:}" [[ -e "$quelle" ]] && info "verschieben: $quelle -> $ziel" done for pfad in "${ENTFERNEN[@]}"; do [[ -e "$pfad" ]] && info "entfernen : $pfad (install.sh legt es unter dem neuen Namen neu an)" done # -- Was belegt den Zielnamen? ----------------------------------------------- # # Nicht nur das Verzeichnis unter /srv: eine Installation der Fassung 1.x # hinterlaesst auch /var/log/, /etc/logrotate.d/ und den # ACME-Pfad. Der Umzug bricht sonst mitten drin ab -- live genau so passiert. BELEGT=() for paar in "${VERSCHIEBEN[@]}"; do ziel="${paar##*:}" [[ -e "$ziel" ]] && BELEGT+=("$ziel") done for weiteres in "/etc/logrotate.d/$TO" "/etc/update-motd.d/50-$TO"; do [[ -e "$weiteres" ]] && BELEGT+=("$weiteres") done BEISEITE="" if [[ ${#BELEGT[@]} -gt 0 ]]; then if [[ -f "/srv/$TO/data/app.db" ]]; then fail "/srv/$TO ist bereits eine Installation dieser Fassung. Welche gewinnt, entscheidet niemand automatisch." fi BEISEITE="/srv/$TO-$SUFFIX" if [[ -e "$BEISEITE" ]]; then # Ein vorhandenes Archiv derselben Verdraengung wird weiterbenutzt -- die # Reste einer Installation gehoeren zusammen und nicht in ein zweites # Verzeichnis. Alles andere unter diesem Namen bleibt unangetastet. if [[ -f "$BEISEITE/sqlite.db" || -d "$BEISEITE/systemd-alt" ]]; then info "Archiv $BEISEITE ist vorhanden und wird weiterbenutzt." else fail "$BEISEITE existiert und sieht nicht nach einem solchen Archiv aus -- bitte --suffix waehlen" fi fi info "Der Zielname ist belegt. Alles davon wandert nach $BEISEITE:" for pfad in "${BELEGT[@]}"; do info " $pfad"; done fi if [[ $DRY_RUN -eq 1 ]]; then step "--dry-run: hier ist Schluss." exit 0 fi if [[ $ASSUME_YES -eq 0 ]]; then read -r -p "Fortfahren? Alle angemeldeten Benutzer werden abgemeldet. [j/N] " answer [[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; } fi # -- 1. Dienste anhalten ----------------------------------------------------- # step "Dienste anhalten" for unit in "$FROM.service" "$FROM-monitor.service"; do if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then systemctl disable --now "$unit" >/dev/null 2>&1 || true info "$unit gestoppt und deaktiviert" fi done # -- 2. Zielnamen freimachen ------------------------------------------------- # if [[ -n "$BEISEITE" ]]; then step "Zielnamen freimachen" # Alles wandert ins Archiv, nichts wird geloescht: unter /var/log/ # liegen Protokolle, unter /srv/ im Zweifel eine ganze Installation. # Die Units und die Site kommen mit -- sie zeigen auf das alte Verzeichnis # und waeren auf dem System nur noch Fehlerquellen, im Archiv aber ein Beleg. mkdir -p "$BEISEITE" for pfad in "${BELEGT[@]}"; do ziel="$BEISEITE/$(basename "$pfad")" if [[ "$pfad" == "/srv/$TO" ]]; then # Das Verzeichnis selbst wird das Archiv -- sein Inhalt bleibt, wie er ist. for eintrag in "$pfad"/* "$pfad"/.[!.]*; do [[ -e "$eintrag" ]] || continue mv "$eintrag" "$BEISEITE/" done rmdir "$pfad" info "/srv/$TO -> $BEISEITE/" continue fi if [[ -e "$ziel" ]]; then ziel="$ziel-$(date +%Y%m%d-%H%M%S)" warn "Im Archiv lag schon etwas mit diesem Namen -- ablegen als $(basename "$ziel")" fi mv "$pfad" "$ziel" info "$pfad -> $ziel" done mkdir -p "$BEISEITE/systemd-alt" for unit in "/etc/systemd/system/$TO.service" "/etc/systemd/system/$TO-check.service" \ "/etc/systemd/system/$TO-check.timer" "/etc/systemd/system/$TO-check-restart.service" \ "/etc/systemd/system/$TO-check-restart.timer"; do [[ -f "$unit" ]] || continue systemctl disable --now "$(basename "$unit")" >/dev/null 2>&1 || true mv "$unit" "$BEISEITE/systemd-alt/" info "$(basename "$unit") archiviert" done for site in "/etc/nginx/sites-enabled/$TO" "/etc/nginx/sites-available/$TO"; do [[ -e "$site" ]] || continue [[ -L "$site" ]] && { rm -f "$site"; continue; } mv "$site" "$BEISEITE/systemd-alt/nginx-site" info "nginx-Site archiviert" done for motd in "/etc/update-motd.d/50-$TO"; do [[ -f "$motd" ]] && grep -q "motd_banner.py" "$motd" 2>/dev/null && rm -f "$motd" \ && info "Banner der Fassung 1.x entfernt" done info "Alles Verdraengte liegt unter $BEISEITE" fi # -- 3. Umziehen ------------------------------------------------------------- # step "Umziehen" for paar in "${VERSCHIEBEN[@]}"; do quelle="${paar%%:*}"; ziel="${paar##*:}" [[ -e "$quelle" ]] || continue [[ -e "$ziel" ]] && fail "$ziel existiert bereits -- Abbruch, damit nichts vermischt wird" mkdir -p "$(dirname "$ziel")" mv "$quelle" "$ziel" info "$quelle -> $ziel" done # -- 4. Namen in den Dateien nachziehen -------------------------------------- # step "Namen nachziehen" ENV_FILE="/etc/tesm/$TO.env" if [[ -f "$ENV_FILE" ]]; then # Der SITE_KEY bestimmt den Namen des Sitzungscookies. Bleibt er alt, heisst # das Cookie weiter wie die alte Instanz -- unauffaellig und falsch. sed -i "s#^${ENV_PREFIX}SITE_KEY=.*#${ENV_PREFIX}SITE_KEY=$TO#" "$ENV_FILE" info "SITE_KEY=$TO" fi # Pfade in der Datenbank, die den Namen enthalten. python3 - "$TO" "$FROM" "/srv/$TO/data/app.db" <<'PY' import sqlite3, sys neu, alt, datenbank = sys.argv[1], sys.argv[2], sys.argv[3] schluessel = ("web_cert_path", "web_key_path", "web_acme_webroot") try: conn = sqlite3.connect(datenbank, timeout=5.0) except sqlite3.Error as fehler: print(f" Datenbank nicht schreibbar: {fehler}") raise SystemExit(0) try: with conn: for key in schluessel: row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone() if not row or not row[0]: continue wert = str(row[0]) ersetzt = wert.replace(alt, neu) if ersetzt != wert: conn.execute("UPDATE settings SET value=? WHERE key=?", (ersetzt, key)) print(f" {key}: {wert} -> {ersetzt}") finally: conn.close() PY # -- 5. Alte Konfiguration entfernen ----------------------------------------- # step "Alte Konfiguration entfernen" for pfad in "${ENTFERNEN[@]}"; do [[ -e "$pfad" ]] || continue rm -f "$pfad" info "$pfad" done systemctl daemon-reload # -- 6. Unter dem neuen Namen einrichten ------------------------------------- # # Das venv wird verworfen und neu gebaut. Es ist abgeleiteter Zustand, und # seine Konsolenskripte tragen den absoluten Pfad im Shebang: nach dem Umzug # zeigt "venv/bin/gunicorn" auf einen Interpreter, den es nicht mehr gibt, und # systemd meldet nur "No such file or directory" -- fuer eine Datei, die # sichtbar da ist. Live auf POETEST genau so passiert. if [[ -d "/srv/$TO/venv" ]]; then step "venv verwerfen (Shebangs tragen den alten Pfad)" rm -rf "/srv/$TO/venv" info "wird von install.sh neu gebaut" fi step "Unter dem neuen Namen einrichten" INSTALL_ARGS=(--app "$APP" --yes) [[ "$TO" != "$APP" ]] && INSTALL_ARGS+=(--instance "${TO#"$APP"-}") bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}" # -- 7. nginx-Site aus den Einstellungen neu schreiben ----------------------- # # install.sh schreibt die Site aus seiner Vorlage, sobald sich die alias-Pfade # aendern -- und die aendern sich hier zwangslaeufig. Der Name der Site steht # aber in der Datenbank, nicht in der Vorlage. Ohne diesen Schritt stuenden # zwei Sites mit "_" auf Port 80 und die erste gewinnt. if [[ -n "$DOMAIN" ]]; then step "Webserver-Konfiguration erneuern" # --apply nicht vergessen: ohne ihn speichert web-setup nur die Einstellungen # und laesst die Site-Datei stehen, die install.sh aus seiner Vorlage # geschrieben hat -- mit "server_name _" und ohne TLS. WEB_ARGS=(web-setup --apply --domain "$DOMAIN") if [[ "$HTTPS_AN" == "1" ]]; then WEB_ARGS+=(--https) [[ "$HSTS_AN" == "1" ]] && WEB_ARGS+=(--hsts) # Das Zertifikat ist mitgezogen und sein Pfad in der Datenbank nachgefuehrt # -- "web-setup --https" laesst es dann unangetastet. Ein neues erzeugen # wuerde nur eine weitere Browserwarnung produzieren. Nur wenn nach dem # Umzug wirklich keines da ist, wird eines erzeugt. CERT_NEU="${CERT_ALT//$FROM/$TO}" if [[ -n "$CERT_NEU" && -f "$CERT_NEU" ]]; then info "HTTPS mit dem vorhandenen Zertifikat: $CERT_NEU" else WEB_ARGS+=(--self-signed) warn "Zertifikat nicht gefunden (${CERT_NEU:-keins hinterlegt}) -- es wird ein neues" warn "selbstsigniertes erzeugt. Der Browser warnt dann einmal erneut." fi fi if sudo -u "$APP" "/usr/local/bin/$TO-admin" "${WEB_ARGS[@]}"; then info "Site neu geschrieben: server_name $DOMAIN" else warn "web-setup ist fehlgeschlagen -- bitte in der Anwendung unter" warn "'Verwaltung -> Webserver und TLS' die Domain eintragen und uebernehmen." fi fi # -- 8. Ergebnis ------------------------------------------------------------- # step "Fertig" info "Verzeichnis : /srv/$TO" info "Dienst : systemctl status $TO.service" info "Werkzeug : $TO-admin" info "Aktualisieren: sudo bash /srv/$TO/deploy/update.sh $APP$([[ "$TO" != "$APP" ]] && echo " latest --instance ${TO#"$APP"-}")" [[ -n "$BEISEITE" ]] && info "Fassung 1.x : $BEISEITE (Units und Site darin unter systemd-alt/)" echo warn "Alle Benutzer sind abgemeldet: das Sitzungscookie heisst jetzt anders."