688 lines
29 KiB
Bash
688 lines
29 KiB
Bash
#!/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/<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
|
|
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 <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.
|
|
--tag <vX.Y.Z> 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 "<name>/", also
|
|
# liegt das Skript unter <name>/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/<app>-sonnet5``, sobald der Hauptname fuer die neue gebraucht wird.
|
|
# Danach liegt unter ``/srv/<app>`` 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 <<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
|