#!/usr/bin/env bash # ============================================================================= # Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme. # # Eine einzelne Datei genuegt -- sie holt sich das Release selbst: # # curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh # sudo bash upgrade.sh --app tesm # # Auf einer 1.2.8 liegt nichts von diesem Projekt; alles andere waere die # falsche Voraussetzung. Liegt das Skript in einem entpackten Release, wird # dessen install.sh benutzt statt eines Downloads: # # sudo bash deploy/upgrade.sh --app tesm # sudo bash deploy/upgrade.sh --app tesm --instance test --source /srv/tesm # # Der Ablauf ist die Vorgabe und braucht keine Schalter: # # 1. **Sichern.** Vollstaendige Kopie der alten Installation nach # /srv/-upgrade-/alt. Erst danach wird irgendetwas # angefasst -- und aus dieser Kopie stammen auch alle weiteren Schritte. # 2. **Altes System abschalten.** Dienst und Timer werden gestoppt und # deaktiviert, damit sie nach einem Neustart nicht zurueckkommen. # 3. **Neu installieren.** install.sh dieses Verzeichnisses. # 4. **Exportieren.** Aus der Kopie entsteht eine Exportdatei im **alten** # Format -- genau die, die der Vorgaenger selbst geschrieben haette. # 5. **Uebersetzen.** Daraus eine Datei im neuen Format. # 6. **Einspielen.** Diese Datei geht durch denselben Import wie eine # Sicherung. # # Warum die Sicherung vor dem Export steht und nicht danach: laeuft das # Upgrade **in dasselbe Verzeichnis** (also ohne --instance), ueberschreibt # install.sh die alte Installation. Wer erst exportiert und dann sichert, # sichert im schlechtesten Fall nichts mehr. Gelesen wird ausschliesslich aus # der Kopie -- das Ergebnis ist dasselbe, die Reihenfolge ist die sichere. # # Der Lizenzserver kennt Schritt 4 und 5 nicht: seine Uebernahme liest die alte # Datenbank unmittelbar und **signiert jede Lizenz neu**. Dabei wird der alte # Signaturschluessel uebernommen -- sonst muesste jeder Kunde einen # Ausstellerwechsel bestaetigen. Wer das nicht will: --new-key. Dort ist die # Kopie aus Schritt 1 der Export. # ============================================================================= set -euo pipefail APP="" INSTANCE="" SOURCE="" PASSPHRASE="" ASSUME_YES=0 DRY_RUN=0 WITH_DATA=1 STOP_OLD=1 ADOPT_KEY=1 TAG="latest" ROLLBACK="" #: Steht die Sicherung schon, und ist der Lauf durch? Daran entscheidet #: die EXIT-Falle, ob sie zurueckrollt. SICHERUNG_STEHT=0 UPGRADE_FERTIG=0 #: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der #: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein #: Hygienetest vergleicht sie. #: #: Auf einer echten 1.2.8 gibt es das **nicht** -- dort liegt die Installation #: unter ihrem Hauptnamen. Der Zusatz entsteht erst, wenn ``rename.sh`` den #: Hauptnamen fuer die neue Fassung braucht. Deshalb ist er der zweite Ort, an #: dem gesucht wird, nicht der erste. ARCHIV_SUFFIX="sonnet5" #: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben? info_quelle="" EXTRA=() SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/.." && 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: upgrade.sh --app [Optionen] --app Welche Anwendung (Pflicht) --source Verzeichnis der alten Installation (Vorgabe: /srv/tesm bzw. /srv/tesm-license) --instance Neue Installation als benannte Instanz anlegen, statt das alte Verzeichnis zu ueberschreiben. Damit bleibt die alte Installation liegen und ist zurueckholbar. --passphrase Passphrase der Zwischendateien. Ohne Angabe wird eine erzeugt und in der Sicherung abgelegt. --no-data Nur installieren, keine Datenuebernahme --keep-old-running Alten Dienst nicht stoppen (nur mit --instance sinnvoll) --new-key Nur Lizenzserver: einen neuen Signaturschluessel erzeugen, statt den alten zu uebernehmen. Dann muessen alle Kunden den Ausstellerwechsel bestaetigen -- die Vorgabe ist deshalb die Uebernahme. --tag Bestimmte Version installieren (Vorgabe: latest). Wird nur beachtet, wenn das Release geladen wird. --rollback [pfad] Einen fehlgeschlagenen Lauf zurueckdrehen: die alte Installation aus der Sicherung wiederherstellen und ihren Dienst wieder einschalten. Ohne Pfad wird die neueste Sicherung dieses Namens genommen. --dry-run Nur zeigen, was geschehen wuerde --yes Keine Rueckfrage -h, --help Diese Hilfe Alles weitere wird unveraendert an install.sh weitergegeben, etwa --https oder --domain. Beispiele: # In dasselbe Verzeichnis, alte Installation wird ersetzt: sudo bash deploy/upgrade.sh --app tesm # Daneben, zum Vergleich, alte Installation bleibt unangetastet: sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running USAGE } while [[ $# -gt 0 ]]; do case "$1" in --app) APP="${2:?}"; shift 2 ;; --source) SOURCE="${2:?}"; shift 2 ;; --instance) INSTANCE="${2:?}"; shift 2 ;; --passphrase) PASSPHRASE="${2:?}"; shift 2 ;; --no-data) WITH_DATA=0; shift ;; --keep-old-running) STOP_OLD=0; shift ;; --new-key) ADOPT_KEY=0; shift ;; --adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit --tag) TAG="${2:?}"; shift 2 ;; --rollback) # Der Pfad ist optional: allein genommen heisst es "die neueste". if [[ -n "${2:-}" && "${2:-}" != --* ]]; then ROLLBACK="$2"; shift 2 else ROLLBACK="auto"; shift; fi ;; --dry-run) DRY_RUN=1; shift ;; --yes|-y) ASSUME_YES=1; shift ;; -h|--help) usage; exit 0 ;; *) EXTRA+=("$1"); shift ;; esac done [[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; } [[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren" NAME="$APP" [[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE" ADMIN="$NAME-admin" NEW_ROOT="/srv/$NAME" STAMP="$(date +%Y%m%d-%H%M%S)" BACKUP="/srv/$NAME-upgrade-$STAMP" ALT_KOPIE="$BACKUP/alt" SYSTEM_KOPIE="$BACKUP/system" EXPORT_ALT="$BACKUP/export-alt.json" IMPORT_NEU="$BACKUP/import-neu.json" # Alte Einheiten der Vorgaengerfassung. Der Timer gehoert dazu: nur den Dienst # zu stoppen liesse ihn beim naechsten Ablauf wieder anlaufen. # -- Rueckweg ---------------------------------------------------------------- # # Alles, was install.sh unter demselben Namen ueberschreiben wuerde. Bei einem # Upgrade in dasselbe Verzeichnis sind das die Dateien der alten Fassung. SYSTEMDATEIEN=( "/etc/systemd/system/$NAME.service" "/etc/systemd/system/$NAME-monitor.service" "/etc/nginx/sites-available/$NAME" "/etc/logrotate.d/$NAME" "/etc/update-motd.d/50-$NAME" "/etc/tesm/$NAME.env" ) zurueckrollen() { local sicherung="$1" grund="${2:-}" local alt="$sicherung/alt" system="$sicherung/system" [[ -d "$alt" ]] || fail "In $sicherung fehlt das Verzeichnis 'alt' -- das ist keine Sicherung dieses Skripts." # Quelle und Art des Umzugs stehen im Manifest. Aus dem laufenden Aufruf # abzuleiten waere falsch: ein Rueckweg kann Tage spaeter und mit anderen # Schaltern aufgerufen werden -- und beim automatischen Rueckweg steht # IN_PLACE zwar fest, beim spaeteren --rollback aber nicht. local ziel="${SOURCE:-}" gleiches_verzeichnis="${IN_PLACE:-0}" vermerkt if [[ -f "$system/manifest" ]]; then vermerkt="$(awk '$1=="quelle" {print $2}' "$system/manifest" | head -1)" [[ -n "$vermerkt" ]] && ziel="$vermerkt" vermerkt="$(awk '$1=="in-place" {print $2}' "$system/manifest" | head -1)" [[ -n "$vermerkt" ]] && gleiches_verzeichnis="$vermerkt" fi [[ -n "$ziel" ]] || fail "Im Manifest fehlt die Quelle -- bitte --source angeben." [[ -n "$grund" ]] && warn "$grund" step "Rueckweg: alten Stand wiederherstellen" # Der neue Dienst zuerst -- er haelt sonst Dateien offen, die gleich ersetzt # werden. for unit in "$NAME.service" "$NAME-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" fi done # Verzeichnis: nur bei gleichem Ziel -- bei einer eigenen Instanz war die # alte Installation nie angefasst. if [[ "$gleiches_verzeichnis" == "1" ]]; then rm -rf "$ziel" mkdir -p "$ziel" tar -C "$alt" -cf - . | tar -C "$ziel" -xf - info "$ziel aus der Sicherung wiederhergestellt" [[ -d "$ziel/venv" ]] || warn "Im Backup war kein venv -- der alte Dienst braucht dann noch 'python3 -m venv venv' und seine requirements.txt." else info "$ziel war nicht betroffen (eigene Instanz) -- bleibt, wie es ist" if [[ -d "$NEW_ROOT" && "$NEW_ROOT" != "$ziel" ]]; then rm -rf "$NEW_ROOT" info "$NEW_ROOT entfernt" fi fi # Systemdateien zurueck, alles andere entfernen, was der Lauf angelegt hat. local site_wieder=0 if [[ -f "$system/manifest" ]]; then while read -r zeile; do case "$zeile" in unit-enabled\ *|site-enabled\ *) continue ;; esac local name; name="$(basename "$zeile")" if [[ -e "$system/$name" ]]; then cp -a "$system/$name" "$zeile" info "wiederhergestellt: $zeile" [[ "$zeile" == /etc/nginx/* ]] && site_wieder=1 fi done < <(grep -v '^\(unit\|site\)-enabled ' "$system/manifest" || true) fi systemctl daemon-reload if [[ $site_wieder -eq 1 ]] || [[ -e "/etc/nginx/sites-enabled/$NAME" ]]; then nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 \ && info "nginx neu geladen" || warn "nginx meldet einen Fehler -- bitte 'nginx -t' ansehen" fi # Und die Dienste wieder einschalten, die vorher liefen. if [[ -f "$system/manifest" ]]; then while read -r _ unit; do systemctl enable --now "$unit" >/dev/null 2>&1 \ && info "$unit wieder eingeschaltet" || warn "$unit liess sich nicht starten" done < <(grep '^unit-enabled ' "$system/manifest" || true) fi step "Zurueckgerollt" info "Sicherung bleibt liegen: $sicherung" # Nicht ueber ALTE_UNITS: die Liste entsteht erst nach der Quellensuche, und # bei "--rollback" laeuft diese Funktion vorher. Das Manifest weiss es. local hinweis hinweis="$(awk '$1=="unit-enabled" {print $2; exit}' "$system/manifest" 2>/dev/null || true)" info "Zustand pruefen: systemctl status ${hinweis:-$APP.service}" } # Wird von der EXIT-Falle des Bootstraps aufgerufen -- mit dem Ausstiegscode. # Damit gilt der Rueckweg fuer **jeden** Fehlschlag und nicht nur fuer den, an # den ich gedacht habe: Export, Uebersetzen, Einspielen, nginx, ein Ctrl-C. beim_beenden() { local code="${1:-0}" [[ "$code" == "0" ]] && return 0 [[ $UPGRADE_FERTIG -eq 1 ]] && return 0 if [[ $SICHERUNG_STEHT -eq 0 ]]; then warn "Abgebrochen (Code $code), bevor eine Sicherung stand -- nichts zurueckzurollen." return 0 fi zurueckrollen "$BACKUP" "Abgebrochen (Code $code)." } neueste_sicherung() { local kandidat kandidat="$(ls -1dt /srv/"$NAME"-upgrade-*/ 2>/dev/null | head -1)" [[ -n "$kandidat" ]] || return 1 printf '%s' "${kandidat%/}" } if [[ -n "$ROLLBACK" ]]; then if [[ "$ROLLBACK" == "auto" ]]; then ROLLBACK="$(neueste_sicherung)" || fail "Keine Sicherung unter /srv/$NAME-upgrade-* gefunden. Pfad angeben." info "Neueste Sicherung: $ROLLBACK" fi [[ -d "$ROLLBACK" ]] || fail "$ROLLBACK gibt es nicht" if [[ $DRY_RUN -eq 1 ]]; then step "--dry-run: es wuerde $ROLLBACK zurueckgerollt." exit 0 fi if [[ $ASSUME_YES -eq 0 ]]; then read -r -p "Stand aus $ROLLBACK wiederherstellen? [j/N] " answer [[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; } fi zurueckrollen "$ROLLBACK" exit 0 fi # Ab hier wird wirklich installiert -- ein Rueckweg hat oben schon # beendet. Erst jetzt braucht es ein Release und eine Quelle. # --8<-- Bootstrap: Release holen -------------------------------------------- # Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden # Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und # beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen, # ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt. # # Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG # (aufgeloest) und $SRC (entpacktes Release). Raeumt sein Arbeitsverzeichnis # beim Beenden auf. # # $REPO gibt es, weil Installationsname und Repositorium auseinanderfallen # koennen: die Master-Ausgabe des Lizenzservers liegt in # "tesm-license-master", die Installation heisst weiter "tesm-license". # # Zwei Fallen des Vorgaengers sind hier vermieden: # * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die # Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein # automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs. # * Nach dem Entpacken wird geprueft, dass ein install.sh existiert. BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}" OWNER="${TESM_RELEASE_OWNER:-alientim}" # Ein privates Repositorium -- tesm-license-master -- gibt sein Release nur mit # Zugriffstoken heraus. Reihenfolge: Umgebungsvariable, dann eine Datei, die # der Betreiber selbst anlegt. Der Installer legt sie nie an: ob ein Token auf # dem Server liegt, ist keine Entscheidung eines Skripts. TOKEN="${TESM_RELEASE_TOKEN:-}" if [[ -z "$TOKEN" && -r /etc/tesm-release.token ]]; then TOKEN="$(tr -d '[:space:]' < /etc/tesm-release.token)" fi # Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar # ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem # Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt -- # endete sie in "command not found", grep bekam nichts zu sehen, und das Skript # machte weiter, als waere alles in Ordnung. hat_crlf() { local cr cr="$(printf '\r')" # -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer # Windows-Buildmaschine aber entscheidend: dort entfernt grep den # Wagenruecklauf beim Lesen und findet deshalb nie einen. LC_ALL=C grep -qU "$cr" "$1" } BOOTSTRAP_WORK="" bootstrap_aufraeumen() { # Den Ausstiegscode zuerst festhalten: alles danach ueberschreibt ihn. local code=$? [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK" # Wer einen eigenen Abschluss braucht -- etwa einen Rueckweg nach einem # Fehlschlag --, definiert "beim_beenden". Ein zweiter "trap ... EXIT" waere # der falsche Weg: er wuerde diesen hier ersetzen, und das entpackte Release # bliebe unter /tmp liegen. declare -F beim_beenden >/dev/null 2>&1 && beim_beenden "$code" return "$code" } trap bootstrap_aufraeumen EXIT release_holen() { local repo="${TESM_RELEASE_REPO:-${REPO:-$APP}}" asset # Leeres Array und "set -u" vertragen sich nur mit dieser Schreibweise. local -a kopf=() [[ -n "$TOKEN" ]] && kopf=(-H "Authorization: token $TOKEN") for werkzeug in curl tar python3 find; do command -v "$werkzeug" >/dev/null 2>&1 || { echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2 return 1 } done BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)" if [[ "$TAG" == "latest" ]]; then echo "==> Ermittle aktuellen Release-Tag ..." TAG="$(curl -fsS ${kopf[0]+"${kopf[@]}"} "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || { echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2 [[ -z "$TOKEN" ]] && echo "Ist das Repositorium privat? Dann TESM_RELEASE_TOKEN setzen." >&2 echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2 return 1 } echo " $TAG" fi asset="$repo-$TAG.tar.gz" echo "==> Lade $asset ..." curl -fsSL ${kopf[0]+"${kopf[@]}"} -o "$BOOTSTRAP_WORK/$asset" \ "$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || { echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2 return 1 } echo "==> Entpacke ..." tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK" # Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "/", also # liegt das Skript unter /deploy/install.sh. Mit "-maxdepth 2" fand die # Suche nie etwas -- und die Fehlermeldung schob es auf den Download. SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \ -printf '%h/..\n' | head -1)" [[ -n "$SRC" ]] || { echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2 return 1 } SRC="$(cd "$SRC" && pwd)" echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..." if hat_crlf "$SRC/deploy/install.sh"; then echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2 echo "'git -c core.autocrlf=false archive'." >&2 return 1 fi } # --8<-- Ende Bootstrap ------------------------------------------------------ # Woher kommt install.sh? Liegt dieses Skript in einem entpackten Release, # nimm dessen Verzeichnis -- wer ein Paket ausgepackt hat, will nicht, dass # ungefragt ein anderes geladen wird. Sonst hol das Release. if [[ -f "$SCRIPT_DIR/install.sh" ]]; then SRC="$REPO_DIR" RELEASE_HERKUNFT="$SRC (entpacktes Release neben diesem Skript)" else release_holen || fail "Das Release liess sich nicht holen." RELEASE_HERKUNFT="$TAG von $BASE/$OWNER" fi # -- Wo liegt die alte Fassung? ---------------------------------------------- # # Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block: # ``rename.sh`` schiebt eine Installation der Fassung 1.x nach # ``/srv/-sonnet5``, sobald der Hauptname fuer die neue gebraucht wird. # Danach liegt unter ``/srv/`` die **laufende** Installation, und die # Vorgabe zeigte ins Leere. Erkannt wird an ``sqlite.db`` im Wurzelverzeichnis # -- die neue Fassung legt ihre Datenbank unter ``data/app.db`` ab. ist_alte_fassung() { [[ -f "$1/sqlite.db" ]]; } if [[ -z "$SOURCE" ]]; then for kandidat in "/srv/$APP" "/srv/$APP-$ARCHIV_SUFFIX"; do if ist_alte_fassung "$kandidat"; then SOURCE="$kandidat"; break; fi done if [[ -z "$SOURCE" ]]; then warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:" warn " /srv/$APP (dort liegt sie normalerweise)" warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie, falls" warn " der Hauptname schon vergeben war)" [[ -f "/srv/$APP/data/app.db" ]] && warn "Unter /srv/$APP liegt die laufende Installation dieser Fassung." fail "Bitte --source setzen." fi info_quelle="gefunden: $SOURCE" fi [[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)" ist_alte_fassung "$SOURCE" || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung" IN_PLACE=0 [[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1 # Die Units der alten Fassung erkennt man daran, dass sie in genau dieses # Verzeichnis zeigen -- nicht am Namen der Anwendung. Nach einem rename.sh # heisst die *laufende* Installation "tesm", und eine Namensliste haette sie # gestoppt und deaktiviert, sobald jemand die archivierte 1.x in eine eigene # Instanz hochzieht. # # Timer und Hilfsdienste tragen keinen Pfad (der Restart-Timer ruft nur # "systemctl restart ..."). Sie kommen ueber den Namen der gefundenen Unit mit. alte_units_finden() { local quelle="$1" gefunden=() datei name runde shopt -s nullglob for datei in /etc/systemd/system/*.service; do grep -qs -- "$quelle/" "$datei" && gefunden+=("$(basename "$datei")") done # Zwei Runden: erst Timer und Helfer, die eine gefundene Unit nennen, dann # noch einmal fuer deren eigene Timer. for runde in 1 2; do for datei in /etc/systemd/system/*.service /etc/systemd/system/*.timer; do name="$(basename "$datei")" [[ " ${gefunden[*]-} " == *" $name "* ]] && continue for treffer in ${gefunden[*]-}; do if grep -qs -- "$treffer" "$datei"; then gefunden+=("$name"); break; fi done done done shopt -u nullglob printf '%s\n' ${gefunden[*]-} } mapfile -t ALTE_UNITS < <(alte_units_finden "$SOURCE") if [[ ${#ALTE_UNITS[@]} -eq 0 ]]; then if [[ "$SOURCE" == "/srv/$APP" ]]; then # Kein Fund, aber die Quelle ist der Hauptpfad: die historischen Namen der # Fassung 1.x als Rueckfall. if [[ "$APP" == "tesm" ]]; then ALTE_UNITS=(tesm.service tesm-check.timer tesm-check.service tesm-check-restart.timer tesm-check-restart.service) else ALTE_UNITS=(tesm-license.service) fi else ALTE_UNITS=() fi fi step "Vorhaben" info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}" info "Neue Fassung : $RELEASE_HERKUNFT" info "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}" info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')" info "Sicherung : $ALT_KOPIE" info "Datenuebernahme: $([[ $WITH_DATA -eq 1 ]] && echo ja || echo nein)" info "Altes System : $([[ $STOP_OLD -eq 1 ]] && echo 'stoppen und deaktivieren' || echo 'laeuft weiter')" if [[ $IN_PLACE -eq 1 && $STOP_OLD -eq 0 ]]; then warn "--keep-old-running bei gleichem Verzeichnis: der alte Dienst laeuft auf Dateien," warn "die gleich ersetzt werden. Das geht schief. Bitte --instance verwenden." 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? [j/N] " answer [[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; } fi # -- 1. Sichern -------------------------------------------------------------- # step "Sichern" mkdir -p "$ALT_KOPIE" "$SYSTEM_KOPIE" chmod 700 "$BACKUP" # Das venv kommt nur mit, wenn in dasselbe Verzeichnis installiert wird -- dort # ersetzt install.sh es, und ohne das alte startet der alte Dienst nach einem # Rueckweg nicht mehr. Bei einer eigenen Instanz bleibt die alte Installation # unangetastet, dann waere es nur Ballast (und seine Shebangs zeigen auf einen # anderen Pfad). if [[ $IN_PLACE -eq 1 ]]; then tar -C "$SOURCE" --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf - info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (mit venv, weil dasselbe Verzeichnis)" else tar -C "$SOURCE" --exclude=venv --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf - info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (ohne venv)" fi [[ -f "$ALT_KOPIE/sqlite.db" ]] || fail "In der Kopie fehlt die Datenbank -- Abbruch" # Und die Systemdateien: bei gleichem Verzeichnis schreibt install.sh Unit, # nginx-Site, logrotate und Banner neu. Ohne Kopie liesse sich der alte Stand # nicht wiederherstellen -- das Verzeichnis allein genuegt nicht. : > "$SYSTEM_KOPIE/manifest" # Woher kam der Stand, und ging es in dasselbe Verzeichnis? Ein spaeterer # Rueckweg darf das nicht raten muessen -- er laeuft womoeglich Tage danach # und mit anderen Schaltern. printf 'quelle %s\n' "$SOURCE" >> "$SYSTEM_KOPIE/manifest" printf 'in-place %s\n' "$IN_PLACE" >> "$SYSTEM_KOPIE/manifest" for pfad in "${SYSTEMDATEIEN[@]}"; do [[ -e "$pfad" ]] || continue cp -a "$pfad" "$SYSTEM_KOPIE/$(basename "$pfad")" printf '%s\n' "$pfad" >> "$SYSTEM_KOPIE/manifest" done # Welche Units liefen, und war die Site eingeschaltet? for unit in "${ALTE_UNITS[@]}"; do systemctl is-enabled "$unit" >/dev/null 2>&1 && printf 'unit-enabled %s\n' "$unit" >> "$SYSTEM_KOPIE/manifest" done [[ -L "/etc/nginx/sites-enabled/$NAME" ]] && printf 'site-enabled %s\n' "$NAME" >> "$SYSTEM_KOPIE/manifest" info "$(grep -c . "$SYSTEM_KOPIE/manifest") Systemdatei(en) und Zustaende vermerkt" SICHERUNG_STEHT=1 # -- 2. Altes System abschalten ---------------------------------------------- # if [[ $STOP_OLD -eq 1 ]]; then step "Altes System abschalten" for unit in "${ALTE_UNITS[@]}"; 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 else step "Altes System bleibt in Betrieb" fi # -- 3. Neu installieren ----------------------------------------------------- # step "Neu installieren" INSTALL_ARGS=(--app "$APP" --yes) [[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE") # Kein eigener Fehlerzweig mehr: die EXIT-Falle rollt bei jedem Fehlschlag # zurueck -- hier, beim Export, beim Einspielen oder bei einem Ctrl-C. bash "$SRC/deploy/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"} if [[ $WITH_DATA -eq 0 ]]; then UPGRADE_FERTIG=1 step "Fertig -- ohne Datenuebernahme" info "Sicherung der alten Installation: $ALT_KOPIE" exit 0 fi command -v "$ADMIN" >/dev/null 2>&1 || fail "$ADMIN nicht gefunden -- die Installation ist unvollstaendig" if [[ -z "$PASSPHRASE" ]]; then PASSPHRASE="$(head -c 24 /dev/urandom | base64 | tr -d '=+/' | cut -c1-24)" # Die Passphrase liegt neben den Dateien, die sie schuetzt. Das ist hier kein # Widerspruch: das alte Format verlangt eine, das Verzeichnis gehoert root # allein (0700), und ohne sie waeren die beiden Dateien spaeter unbrauchbar -- # auch als Rueckweg. printf '%s\n' "$PASSPHRASE" > "$BACKUP/passphrase.txt" chmod 600 "$BACKUP/passphrase.txt" fi if [[ "$APP" == "tesm" ]]; then # -- 4. Exportieren -------------------------------------------------------- # step "Daten der alten Installation lesen" "$ADMIN" legacy-export --source "$ALT_KOPIE" --out "$EXPORT_ALT" \ --passphrase "$PASSPHRASE" --force chmod 600 "$EXPORT_ALT" # -- 5. Uebersetzen -------------------------------------------------------- # step "Uebersetzen" "$ADMIN" convert-legacy "$EXPORT_ALT" --ausgabe "$IMPORT_NEU" \ --passphrase "$PASSPHRASE" --neue-passphrase "$PASSPHRASE" --force chmod 600 "$IMPORT_NEU" # -- 6. Einspielen --------------------------------------------------------- # step "Einspielen" "$ADMIN" legacy-import "$IMPORT_NEU" --passphrase "$PASSPHRASE" --actor upgrade else # -- 4.-6. in einem: die Uebernahme liest die alte Datenbank unmittelbar --- # step "Lizenzbestand uebernehmen" # Der alte Signaturschluessel wird uebernommen. Fuer ein Upgrade ist das # die einzige sinnvolle Vorgabe: es ist derselbe Server mit neuerer Software, # und ohne den alten Schluessel muesste jeder Kunde den Ausstellerwechsel # bestaetigen. Ueberschrieben wird dabei nichts -- die Uebernahme lehnt ab, # wenn die neue Installation schon einen eigenen Schluessel hat. MIGRATE_ARGS=(migrate-legacy --source "$ALT_KOPIE") [[ $ADOPT_KEY -eq 1 ]] && MIGRATE_ARGS+=(--adopt-key) "$ADMIN" "${MIGRATE_ARGS[@]}" if [[ $ADOPT_KEY -eq 0 ]]; then warn "--new-key: es gilt ein neuer Signaturschluessel. Die Kunden muessen den" warn "Ausstellerwechsel einmal bestaetigen." fi fi # -- Abschluss --------------------------------------------------------------- # UPGRADE_FERTIG=1 step "Fertig" info "Sicherung der alten Installation : $ALT_KOPIE" if [[ "$APP" == "tesm" ]]; then info "Export im alten Format : $EXPORT_ALT" info "Uebersetzte Datei : $IMPORT_NEU" [[ -f "$BACKUP/passphrase.txt" ]] && info "Passphrase beider Dateien : $BACKUP/passphrase.txt" fi cat <