upgrade.sh -- ein Weg vom Stand SONNET5 auf diese Fassung, mit Daten. Der Ablauf braucht keine Schalter: alte Installation sichern, abschalten, neu installieren, exportieren, uebersetzen, einspielen. Die Sicherung steht bewusst vor dem Export -- ohne --instance installiert install.sh in dasselbe Verzeichnis, und wer erst exportiert und dann sichert, sichert im schlechtesten Fall nichts mehr. Gelesen wird nur aus der Kopie. Dafuer zwei neue Befehle: "legacy-export" liest eine alte Installation unmittelbar (der Vorgaenger kann nur ueber die Weboberflaeche exportieren -- fuer ein Upgrade der falsche Weg, und unmoeglich, wenn die alte Anwendung nicht mehr startet) und schreibt eine Datei im alten Format, die deshalb auch als Rueckweg taugt. "legacy-import" liest eine Datei im neuen Format ein, ueber transfer.apply_import -- dieselbe Funktion wie die Seite "Sicherung", kein zweiter Schreibpfad. Am Lizenzserver hat der Lauf gegen echte Altdaten zwei Fehler gezeigt, die jede Uebernahme unmoeglich machten: * Der alte Signaturschluessel ist URL-sicher kodiert (- und _). Das neue Protokoll prueft Base64 streng -- richtig so, aber beim Lesen einer alten Datei brach die Uebernahme mit einem Traceback ueber "kein gueltiges Base64" ab. Umkodiert wird jetzt an der Grenze zum Altformat; der kanonische Decoder bleibt streng. * Der Anbieterblock wurde aus den Einstellungen der *neuen* Installation gebaut, und ohne Endpunkt brach die Uebernahme ab. Bei einem Upgrade ist die neue Installation frisch -- dort steht nichts. Endpunkt und Anbieterdaten kommen jetzt aus der alten Installation, aber nur dort, wo neu nichts steht. Der alte Signaturschluessel wird uebernommen: sonst muesste jeder Kunde einen Ausstellerwechsel bestaetigen. Leerlaufabmeldung: sie hat nie ausgeloest. Ein offener Tab fragt seine Kacheln alle 15 Sekunden neu ab, und jede Abfrage verlaengerte das Leerlauffenster -- nachweisbar an einer Sitzung, deren last_seen_at eine Minute alt war, obwohl seit zehn Stunden niemand am Rechner sass. Der Client kennzeichnet automatische Anfragen jetzt mit einem Kopf, der Server verlaengert daraufhin nichts, und last_seen_at bleibt, was der Name sagt. Faellt die Sitzung waehrend des Pollens, antwortet der Server mit 401 statt mit einer Umleitung: der Tab laedt neu und zeigt die Anmeldemaske, statt stumm mit alten Zahlen stehenzubleiben. Im Audit ist der Name der Mitschrift jetzt ein Verweis auf die Datei -- es ist derselbe Weg wie zuvor, nur einen Klick weit. Verlinkt wird nur, wenn die Datei existiert und der Betrachter Protokolle lesen darf; der Rest des Eintrags wird maskiert, denn ein Auditeintrag enthaelt Fremdeingaben. Ausserdem: Version 2.0.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
12 KiB
Bash
289 lines
12 KiB
Bash
#!/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/<name>-upgrade-<zeitstempel>/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
|
|
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 <tesm|tesm-license> [Optionen]
|
|
|
|
--app <name> Welche Anwendung (Pflicht)
|
|
--source <pfad> Verzeichnis der alten Installation
|
|
(Vorgabe: /srv/tesm bzw. /srv/tesm-license)
|
|
--instance <name> Neue Installation als benannte Instanz anlegen, statt
|
|
das alte Verzeichnis zu ueberschreiben. Damit bleibt die
|
|
alte Installation liegen und ist zurueckholbar.
|
|
--passphrase <text> 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"
|
|
|
|
[[ -n "$SOURCE" ]] || SOURCE="/srv/$APP"
|
|
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)"
|
|
[[ -f "$SOURCE/sqlite.db" ]] || 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 "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 <<TEXT
|
|
|
|
Von Hand nachzuziehen -- das laesst sich nicht uebersetzen:
|
|
* Lokale Konten: der alte Hash ist bcrypt, hier gilt Argon2id. Ein Hash
|
|
ist nicht umrechenbar; das ist sein Zweck. Erstes Konto anlegen mit
|
|
sudo -u $APP $ADMIN create-admin
|
|
* Gruppen und Rechte: die Rechteschluessel wurden neu geschnitten.
|
|
* Verzeichnisdienst (LDAP), Webserver und Zertifikat.
|
|
TEXT
|
|
|
|
# Der Rueckweg sieht in den beiden Faellen verschieden aus. Ein Hinweis, der
|
|
# das alte Verzeichnis wiederherstellen laesst, obwohl es nie angefasst wurde,
|
|
# waere schlimmer als keiner: er wuerde die Daten der neuen Installation mit
|
|
# einem alten Stand ueberschreiben.
|
|
if [[ $IN_PLACE -eq 1 ]]; then
|
|
cat <<TEXT
|
|
|
|
Zurueck auf den alten Stand -- das Verzeichnis wurde ersetzt, die Kopie
|
|
bringt es zurueck:
|
|
sudo systemctl disable --now $NAME
|
|
sudo rm -rf $SOURCE && sudo mkdir -p $SOURCE
|
|
sudo tar -C $ALT_KOPIE -cf - . | sudo tar -C $SOURCE -xf -
|
|
# venv der alten Installation neu bauen:
|
|
sudo python3 -m venv $SOURCE/venv
|
|
sudo $SOURCE/venv/bin/python -m pip install -r $SOURCE/requirements.txt
|
|
sudo systemctl enable --now ${ALTE_UNITS[0]}
|
|
|
|
TEXT
|
|
else
|
|
cat <<TEXT
|
|
|
|
Zurueck auf den alten Stand -- $SOURCE wurde nicht angefasst, es genuegt,
|
|
die neue Instanz abzuschalten:
|
|
sudo systemctl disable --now $NAME $NAME-monitor
|
|
sudo rm -f /etc/nginx/sites-enabled/$NAME && sudo systemctl reload nginx
|
|
sudo systemctl enable --now ${ALTE_UNITS[0]}
|
|
|
|
TEXT
|
|
fi
|