#!/usr/bin/env bash # ============================================================================= # Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme. # # Aufruf (aus dem entpackten Release-Verzeichnis): # sudo bash deploy/upgrade.sh --app tesm # sudo bash deploy/upgrade.sh --app tesm-license # sudo bash deploy/upgrade.sh --app tesm --instance opus --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 #: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der #: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein #: Hygienetest vergleicht sie. 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. --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 opus --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 --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" [[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript" # -- 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" warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie)" [[ -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" NAME="$APP" [[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE" ADMIN="$NAME-admin" NEW_ROOT="/srv/$NAME" IN_PLACE=0 [[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1 STAMP="$(date +%Y%m%d-%H%M%S)" BACKUP="/srv/$NAME-upgrade-$STAMP" ALT_KOPIE="$BACKUP/alt" 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. 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 step "Vorhaben" info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}" 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" chmod 700 "$BACKUP" # -a: Rechte und Zeiten bleiben, damit die Kopie auch als Rueckweg taugt. # Das venv der alten Installation bleibt aussen vor -- es ist nachbaubar, # gross, und seine Shebangs zeigen ohnehin auf den alten Pfad. tar -C "$SOURCE" --exclude=venv --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf - info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (ohne venv)" [[ -f "$ALT_KOPIE/sqlite.db" ]] || fail "In der Kopie fehlt die Datenbank -- Abbruch" # -- 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") bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"} if [[ $WITH_DATA -eq 0 ]]; then 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 --------------------------------------------------------------- # 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 <