diff --git a/README.md b/README.md index ef483d9..355a520 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ Ausgabe und Verwaltung der Lizenzen für TESM: Kunde -> Ticket -> Lizenz, Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand über MS Graph und eine Hierarchie aus Unter-Lizenzservern. +> **Diese Ausgabe ist für Sub-Lizenzserver.** Ein Sub-Lizenzserver bekommt +> seine eigene Lizenz von der Wurzelinstanz und stellt danach selbst +> Kundenlizenzen aus. Was er nicht kann: sich selbst die Rolle `master` geben -- +> der Code dafür liegt nicht in diesem Paket, sondern nur in der Master-Ausgabe +> (privates Repository `tesm-license-master`). Das ist keine Einstellung, die +> sich umlegen lässt, sondern ein fehlendes Verzeichnis. + **Dieses Repository enthält genau das, was zum Installieren und Betreiben des Lizenzservers gebraucht wird** -- nichts darüber hinaus: @@ -21,6 +28,7 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin. | Wo sonst noch | | |---|---| | [alientim/tesm](https://gitea.int.eertmoed.net/alientim/tesm) | TESM selbst | +| [alientim/tesm-license-master](https://gitea.int.eertmoed.net/alientim/tesm-license-master) | Master-Ausgabe (privat): die Wurzelinstanz | | [alientim/TESM-DEV](https://gitea.int.eertmoed.net/alientim/TESM-DEV) | Entwicklung: beide Anwendungen, Tests, Architekturdoku | ## Zweige @@ -34,16 +42,24 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin. ## Installation +Eine einzelne Datei genügt -- sie lädt das Release selbst: + ```bash -sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.de -sudo -u tesm-license tesm-license-admin init-key -sudo -u tesm-license tesm-license-admin bootstrap +curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm-license/raw/branch/main/install.sh +sudo bash install.sh --app tesm-license --https --domain lizenz.firma.de sudo -u tesm-license tesm-license-admin create-admin ``` -**`init-key` erzeugt den Signaturschlüssel dieses Servers. Sichern Sie ihn -sofort.** Ohne ihn lässt sich keine ausgestellte Lizenz mehr erneuern, und -Kunden müssten einen Ausstellerwechsel bestätigen. +Aus einem entpackten Paket ist es derselbe Aufruf ohne `curl`. + +Den **Signaturschlüssel** legt der Installer selbst an -- mit ihm signiert +dieser Server seine Kundenlizenzen. **Sichern Sie ihn sofort** +(`data/master_signing_key.json`): ohne ihn ist keine ausgestellte Lizenz mehr +prüfbar, und Kunden müssten einen Ausstellerwechsel bestätigen. Eine +Aktualisierung fasst ihn nie an. + +Die **eigene Lizenz** dieses Servers kommt von der Wurzelinstanz: dort eine +Lizenz für diesen Server anfordern und unter "Eigene Lizenz" einspielen. Zielzustand: **Port 443 mit TLS, Port 80 leitet dorthin um** -- ausser dem ACME-Pfad, der erreichbar bleiben muss, sonst kann Let's Encrypt nie erneuern. @@ -57,7 +73,7 @@ für nginx-Site, logrotate, Zertifikate und Sitzungscookie. ### Aktualisieren ```bash -sudo bash deploy/update.sh --app tesm-license +sudo bash /srv/tesm-license/deploy/update.sh tesm-license ``` Vor jedem Update entsteht ein vollständiges Backup, danach läuft ein @@ -70,7 +86,6 @@ Health-Check mit automatischem Rollback. ```bash sudo -u tesm-license tesm-license-admin status sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel -sudo -u tesm-license tesm-license-admin bootstrap # Grunddaten sudo -u tesm-license tesm-license-admin create-admin sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand übernehmen ``` diff --git a/apps/tesm-license/src/tesm_license/__init__.py b/apps/tesm-license/src/tesm_license/__init__.py index 42470c5..5dd39a1 100644 --- a/apps/tesm-license/src/tesm_license/__init__.py +++ b/apps/tesm-license/src/tesm_license/__init__.py @@ -8,6 +8,7 @@ musste (und ``licensing.py`` byte-identisch gehalten werden *musste*). from __future__ import annotations +import importlib.util import logging from pathlib import Path from typing import Any @@ -51,6 +52,18 @@ ENV_PREFIX = "TESM_LICENSE_" logger = logging.getLogger(__name__) +def is_master_edition() -> bool: + """Ist das hier die Master-Ausgabe? + + Entscheidet die **Anwesenheit** des Teilpakets ``tesm_license.master``, nicht + eine Einstellung: eine Einstellung koennte der lokale Administrator eines + Sub-Lizenzservers umstellen, eine fehlende Datei nicht. Das Release fuer + Sub-Lizenzserver bringt das Teilpaket nicht mit -- dort gibt es also + ueberhaupt keinen Code, der die Rolle ``master`` erzeugen koennte. + """ + return importlib.util.find_spec("tesm_license.master") is not None + + def master_key_path(config: Any) -> Path: return Path(config.data_dir) / "master_signing_key.json" diff --git a/apps/tesm-license/src/tesm_license/blueprints/own_license.py b/apps/tesm-license/src/tesm_license/blueprints/own_license.py index 52fd2f9..4c3c9a7 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/own_license.py +++ b/apps/tesm-license/src/tesm_license/blueprints/own_license.py @@ -32,6 +32,7 @@ from tesm_core.licensing_client import LICENSED_FEATURES, LicenseError import tesm_licensing as lic +from .. import is_master_edition from ..services import master_key from .common import can_issue_license_server_licenses, log_event, signing_key @@ -62,6 +63,7 @@ def overview(): ), signing_key_health=master_key.health(extension.registry["master_key_path"]), may_issue_license_server=can_issue_license_server_licenses(), + master_edition=is_master_edition(), grace_days=lic.GRACE_PERIOD_DAYS, ) diff --git a/apps/tesm-license/src/tesm_license/cli.py b/apps/tesm-license/src/tesm_license/cli.py index 4bb7fe9..2cb5cca 100644 --- a/apps/tesm-license/src/tesm_license/cli.py +++ b/apps/tesm-license/src/tesm_license/cli.py @@ -1,13 +1,14 @@ """Kommandozeilenwerkzeuge des Lizenzservers. -Enthaelt insbesondere ``bootstrap`` -- die eigene Lizenz der Wurzelinstanz. +``init-key`` erzeugt den Signaturschluessel dieser Installation -- den braucht +**jeder** Lizenzserver, um Kundenlizenzen zu signieren, auch ein +Sub-Lizenzserver. -Warum das ein lokales Skript ist und keine Funktion in der Oberflaeche: die -Master-Lizenz ist die einzige Lizenz, fuer die es **keine hoehere Instanz gibt**, -die sie bestaetigen koennte. Ein Aktivierungs-Handshake gegen sich selbst waere -Theater. Das Skript signiert die Datei mit dem eigenen Schluessel und setzt den -Aktivierungszustand direkt -- und nur so kann eine Lizenzdatei je die Rolle -``master`` tragen. Die Ticket-Oberflaeche bietet diese Rolle an keiner Stelle an. +``bootstrap`` stellt die eigene Lizenz der Wurzelinstanz aus und ist damit der +einzige Weg, auf dem eine Lizenzdatei die Rolle ``master`` bekommt (ueber +Tickets ist sie auf jedem Server abgewiesen). Die Arbeit dazu liegt im +Teilpaket ``tesm_license.master``, das nur die Master-Ausgabe mitbringt; fehlt +es, sagt der Befehl das und tut nichts. Siehe dort fuer die Begruendung. """ from __future__ import annotations @@ -20,7 +21,6 @@ from pathlib import Path from typing import Any import tesm_licensing as lic -import tesm_licensing_server as licsrv from tesm_core import audit from tesm_core.cli_web import add_web_setup, run_web_setup from tesm_core.db import Database @@ -28,7 +28,7 @@ from tesm_core.extension import core from tesm_core.rbac import service as rbac from tesm_core.security import passwords -from . import create_app, master_key_path +from . import create_app, is_master_edition, master_key_path from .services import legacy_import, master_key @@ -62,73 +62,29 @@ def cmd_init_key(args: argparse.Namespace) -> int: def cmd_bootstrap(args: argparse.Namespace) -> int: - """Stellt die eigene Lizenz dieser Wurzelinstanz aus.""" + """Stellt die eigene Lizenz dieser Wurzelinstanz aus -- nur in der Master-Ausgabe.""" + if not is_master_edition(): + for zeile in ( + "Diese Ausgabe ist fuer Sub-Lizenzserver und kann keine Master-Lizenz erzeugen.", + "", + "Ein Sub-Lizenzserver bekommt seine Lizenz vom Master-Lizenzserver und spielt", + "sie unter 'Eigene Lizenz' ein. Danach stellt er selbst Kundenlizenzen aus --", + "mit seinem eigenen Signaturschluessel, der beim Installieren entstanden ist.", + "", + "Die Master-Ausgabe liegt im Repository tesm-license-master.", + ): + print(zeile, file=sys.stderr) + return 2 + + from .master import bootstrap as eigene_lizenz_ausstellen + app, extension = _app_and_core() - key = master_key.ensure(master_key_path(extension.config)) - - with app.app_context(), extension.database.session() as conn: - vendor = { - "name": str(extension.settings.get(conn, "vendor_name") or "Eigenbetrieb"), - "email": str(extension.settings.get(conn, "vendor_email") or ""), - "phone": str(extension.settings.get(conn, "vendor_phone") or ""), - "address": str(extension.settings.get(conn, "vendor_address") or ""), - "logo_base64": "", - } - bundle, _ = licsrv.issue_license( - customer={"name": args.name, "contact_email": vendor["email"]}, - license_type="enterprise", - modules=[], - valid_days=int(args.days), - master_private_key_b64=key.private_key, - master_public_key_b64=key.public_key, - # Bewusst leer: es gibt keinen uebergeordneten Endpunkt. Damit - # laeuft der Handshake fuer diese Lizenz nie ins Netz. - master_endpoint="", - vendor=vendor, - role="master", - ) - document, client_key = lic.split_bundle(bundle) - - config = extension.config - config.license_path.parent.mkdir(parents=True, exist_ok=True) - config.license_path.write_text( - json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n", - encoding="utf-8", - ) - try: - config.license_path.chmod(0o640) - except OSError: - pass - from tesm_core.keystore import write_private_file - - write_private_file( - config.license_key_path, - json.dumps({"license_id": document["license_id"], "client_key": client_key}) + "\n", - ) - - with extension.database.transaction(conn): - conn.execute( - "INSERT INTO trusted_issuer_keys (key_id, public_key, label, source, added_at, " - "added_by) VALUES (?,?,?,'bootstrap',datetime('now'),'cli') " - "ON CONFLICT(key_id) DO NOTHING", - (key.key_id, key.public_key, "Eigener Signaturschluessel"), - ) - # Es gibt keine hoehere Instanz, die diese Lizenz aktivieren koennte. - extension.settings.set(conn, "license_activation_status", "active", actor="cli") - extension.settings.set(conn, "license_pending_request", "", actor="cli") - audit.log( - conn, - actor="cli", - action="own_license.bootstrapped", - target=document["license_id"], - detail=f"Rolle master, {args.days} Tage, Schluessel {key.key_id}", - severity="warning", - ) - state = extension.license.reload(conn) if extension.license else None - + document, state = eigene_lizenz_ausstellen( + app, extension, days=int(args.days), name=args.name + ) print(f"Master-Lizenz ausgestellt: {document['license_id']}") print(f"Gueltig bis: {document['expires_at']}") - print(f"Datei: {config.license_path}") + print(f"Datei: {extension.config.license_path}") if state is not None: print(f"Zustand: gueltig={state.valid}, aktiviert={state.activated}, Rolle={state.role}") return 0 @@ -434,7 +390,12 @@ def build_parser() -> argparse.ArgumentParser: init_key.set_defaults(func=cmd_init_key) bootstrap = sub.add_parser( - "bootstrap", help="Eigene Master-Lizenz dieser Wurzelinstanz ausstellen" + "bootstrap", + help=( + "Eigene Master-Lizenz dieser Wurzelinstanz ausstellen" + if is_master_edition() + else "nicht in dieser Ausgabe (nur Master-Lizenzserver)" + ), ) bootstrap.add_argument("--days", type=int, default=3650) bootstrap.add_argument("--name", default="Lizenzserver (Eigenbetrieb)") diff --git a/apps/tesm-license/src/tesm_license/templates/license/own_license.html b/apps/tesm-license/src/tesm_license/templates/license/own_license.html index 823cec6..16761a3 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/own_license.html +++ b/apps/tesm-license/src/tesm_license/templates/license/own_license.html @@ -116,9 +116,16 @@ {% endif %} {% else %} - {{ empty_state('Keine gültige Lizenz', - 'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap" - die eigene Lizenz der Wurzelinstanz.', 'key') }} + {% if master_edition %} + {{ empty_state('Keine gültige Lizenz', + 'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap" + die eigene Lizenz der Wurzelinstanz.', 'key') }} + {% else %} + {{ empty_state('Keine gültige Lizenz', + 'Diese Ausgabe ist ein Sub-Lizenzserver: fordern Sie beim Master-Lizenzserver eine + Lizenz für diesen Server an und spielen Sie die Datei hier ein. Eine Master-Lizenz + kann diese Ausgabe nicht erzeugen.', 'key') }} + {% endif %} {% endif %} {% if pending %} diff --git a/deploy/install.sh b/deploy/install.sh index a2d718b..6a4bd06 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -28,6 +28,9 @@ set -euo pipefail APP="" INSTANCE="" +#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die +#: Portsuche in einem Test wirklich ausfuehren laesst. +ETC_TESM="${TESM_ETC_DIR:-/etc/tesm}" PORT="" HTTP_PORT="" SKIP_PACKAGES=0 @@ -46,6 +49,62 @@ 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' "$*"; } +#: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist. +SICHERUNG_STEHT=0 +INSTALL_FERTIG=0 +ZURUECKGEROLLT=0 + +zurueckrollen_install() { + [[ $SICHERUNG_STEHT -eq 1 && $ZURUECKGEROLLT -eq 0 ]] || return 0 + # Kommt nur zum Zug, wenn die Sicherung steht -- dann sind diese Variablen + # gesetzt. Der Vollstaendigkeit halber trotzdem gepruefte Namen. + [[ -n "${BACKUP_DIR:-}" && -d "$BACKUP_DIR" ]] || { warn "Keine Sicherung gefunden -- nichts wiederhergestellt."; return 0; } + ZURUECKGEROLLT=1 + warn "Alten Stand wiederherstellen." + systemctl stop "$UNIT" 2>/dev/null || true + [[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true + rm -rf "$FAILED_DIR" + mv "$ROOT" "$FAILED_DIR" 2>/dev/null || true + cp -a "$BACKUP_DIR" "$ROOT" + systemctl start "$UNIT" 2>/dev/null || true + [[ "$APP" == "tesm" ]] && systemctl start "$MONITOR_UNIT" 2>/dev/null || true + warn "Wiederhergestellt aus $BACKUP_DIR" + warn "Fehlgeschlagener Stand liegt unter $FAILED_DIR" +} + +# Bei **jedem** Fehlschlag, nicht nur bei einem, an den ich gedacht habe: +# Systempakete, venv, sudoers, nginx, ein Ctrl-C. Vorher blieb der Dienst +# gestoppt und das Verzeichnis teils ersetzt zurueck. +bei_fehler() { + local code=$? + [[ $INSTALL_FERTIG -eq 1 || "$code" == "0" ]] && return "$code" + if [[ $SICHERUNG_STEHT -eq 1 ]]; then + zurueckrollen_install + else + # Neuinstallation: es gibt keinen alten Stand. Nichts loeschen -- im + # Zielverzeichnis kann eine Fassung 1.x liegen, deren Sicherung ein + # anderes Skript haelt (upgrade.sh). + # Vorsichtige Ersetzung: bricht es schon beim Auswerten der Argumente ab, + # gibt es $ROOT noch nicht -- und "set -u" wuerde die Falle selbst zum + # Fehler machen. + if [[ ${IS_UPDATE:-0} -eq 1 ]]; then + # Abbruch vor dem Sicherungsschritt: die Installation ist unangetastet. + warn "Abgebrochen (Code $code), bevor etwas geaendert wurde." + warn "Die bestehende Installation ist unberuehrt -- nichts zurueckzurollen." + else + warn "Abgebrochen (Code $code). Es gab keinen vorherigen Stand dieser Fassung," + warn "also wurde nichts wiederhergestellt und nichts geloescht." + fi + # Nur sagen, was es wirklich gibt: bei einem Abbruch an der Rueckfrage + # existiert das Verzeichnis noch nicht, und die Zeile log dann. + if [[ -n "${ROOT:-}" && -d "${ROOT:-}" ]]; then + warn "Angelegt wurde bis hierhin: $ROOT (Daten unter data/ unberuehrt)." + fi + fi + return "$code" +} +trap bei_fehler EXIT + usage() { cat <<'USAGE' Verwendung: install.sh --app [Optionen] @@ -112,13 +171,24 @@ MONITOR_UNIT="$NAME-monitor.service" NGINX_SITE="/etc/nginx/sites-available/$NAME" ENV_PREFIX="TESM_" [[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_" + +# Ausgabe des Lizenzservers: die Master-Ausgabe bringt das Teilpaket +# tesm_license/master mit -- den einzigen Weg, auf dem eine Lizenzdatei die +# Rolle "master" bekommt. Das Release fuer Sub-Lizenzserver hat es nicht. +# Die Ausgabe haengt an der Anwesenheit des Verzeichnisses, nicht an einer +# Einstellung: eine Einstellung liesse sich vor Ort umstellen. +MASTER_TEILPAKET="apps/tesm-license/src/tesm_license/master" +EDITION="standard" +if [[ "$APP" == "tesm-license" ]]; then + if [[ -d "$REPO_DIR/$MASTER_TEILPAKET" ]]; then EDITION="master"; else EDITION="sub"; fi +fi # Bei einer Aktualisierung gilt zuerst, was schon eingerichtet ist. Sonst # verschiebt ein Update ohne Portangaben die Anwendung: eine benannte Instanz # faellt auf 8080/8443 zurueck, auch wenn sie seit der Installation auf Port 80 # lief -- und ein Reverse Proxy davor zeigt dann ins Leere. Dieselbe Regel wie # beim Secure-Flag weiter unten: eine Aktualisierung nimmt nichts weg, was # nicht ausdruecklich neu gesetzt wird. -ENV_FILE="/etc/tesm/$NAME.env" +ENV_FILE="$ETC_TESM/$NAME.env" if [[ -z "$PORT" && -f "$ENV_FILE" ]]; then PORT="$(sed -n "s/^${ENV_PREFIX}BIND=127\.0\.0\.1:\([0-9]\{1,5\}\)$/\1/p" "$ENV_FILE" | head -1)" fi @@ -127,10 +197,30 @@ if [[ -f "$NGINX_SITE" ]]; then [[ -z "$HTTPS_PORT" ]] && HTTPS_PORT="$(sed -n 's/^[[:space:]]*listen[[:space:]]\([0-9]\{1,5\}\)[[:space:]]\+ssl.*/\1/p' "$NGINX_SITE" | head -1)" fi +port_belegt() { + local p="$1" + # Schon von einer anderen Installation vergeben? Die Umgebungsdateien sind + # die Wahrheit -- ein Dienst kann gerade gestoppt sein und den Port trotzdem + # beanspruchen. + grep -qs "BIND=127\.0\.0\.1:$p\$" "$ETC_TESM"/*.env 2>/dev/null && + return 0 + # Oder gerade in Benutzung, von was auch immer. + command -v ss >/dev/null 2>&1 && [[ -n "$(ss -Hltn "sport = :$p" 2>/dev/null)" ]] && return 0 + return 1 +} + if [[ -z "$PORT" ]]; then - PORT=5000 - [[ "$APP" == "tesm-license" ]] && PORT=5001 - [[ -n "$INSTANCE" ]] && PORT=$((PORT + 100)) + BASISPORT=5000 + [[ "$APP" == "tesm-license" ]] && BASISPORT=5001 + if [[ -n "$INSTANCE" ]]; then + # Frueher bekam **jede** Instanz Basis+100. Die zweite startete nie + # ("Address already in use") -- und die Gesundheitspruefung merkte es + # nicht, weil die erste auf demselben Port antwortete. + PORT=$((BASISPORT + 100)) + while port_belegt "$PORT"; do PORT=$((PORT + 1)); done + else + PORT=$BASISPORT + fi fi if [[ -z "$HTTP_PORT" ]]; then HTTP_PORT=80 @@ -212,12 +302,24 @@ fi # Der Vorgaenger lief als root. Hier laeuft der Dienst unprivilegiert; Rootrechte # gibt es ausschliesslich ueber den Helfer. +# Ein Sub-Paket auf eine Master-Installation waere ein stiller Rueckbau: der +# Abgleich unten laeuft mit --delete und wuerde das Teilpaket entfernen. Danach +# gaebe es auf dem Master keinen Weg mehr, eine Master-Lizenz auszustellen -- +# und niemand haette es gesagt. +if [[ "$EDITION" == "sub" && -d "$ROOT/$MASTER_TEILPAKET" ]]; then + fail "Hier laeuft die Master-Ausgabe, dieses Paket ist die Ausgabe fuer Sub-Lizenzserver. + Eine Aktualisierung damit wuerde die Master-Ausgabe entfernen. + Passendes Paket: Repository tesm-license-master (privat). + Bewusster Rueckbau: $ROOT/$MASTER_TEILPAKET von Hand entfernen, dann erneut." +fi + # -- Backup ------------------------------------------------------------------ if [[ $IS_UPDATE -eq 1 ]]; then step "Sicherung vor der Aktualisierung" systemctl stop "$UNIT" 2>/dev/null || true [[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true cp -a "$ROOT" "$BACKUP_DIR" + SICHERUNG_STEHT=1 info "Sicherung: $BACKUP_DIR" fi @@ -347,8 +449,8 @@ step "Umgebung" # EnvironmentFile, die Kommandozeilenwerkzeuge ueber ihren Wrapper. Ohne das # wuesste "tesm-admin" nicht, wo die Instanz liegt, und wuerde sich eine leere # Datenbank im Arbeitsverzeichnis anlegen. -install -d -o root -g root -m 0755 /etc/tesm -ENV_FILE="/etc/tesm/$NAME.env" +install -d -o root -g root -m 0755 "$ETC_TESM" +ENV_FILE="$ETC_TESM/$NAME.env" TMP_ENV="$(mktemp)" { echo "# Von install.sh erzeugt fuer $NAME -- nicht von Hand bearbeiten." @@ -491,7 +593,11 @@ fi step "Gesundheitspruefung" HEALTHY=0 for attempt in $(seq 1 20); do - if curl -fsS --max-time 3 "http://127.0.0.1:$PORT/gesundheit" >/dev/null 2>&1; then + # Beides: der Dienst muss laufen **und** antworten. Nur die Antwort zu + # pruefen ging schon einmal schief -- auf einem gemeinsamen Port antwortete + # eine andere Instanz, und ein toter Dienst galt als gesund. + if systemctl is-active --quiet "$UNIT" && + curl -fsS --max-time 3 "http://127.0.0.1:$PORT/gesundheit" >/dev/null 2>&1; then HEALTHY=1 info "Antwort nach ${attempt}x" break @@ -502,13 +608,9 @@ done if [[ $HEALTHY -eq 0 ]]; then warn "Die Anwendung antwortet nicht." if [[ $IS_UPDATE -eq 1 ]]; then - warn "Rollback wird ausgefuehrt." - systemctl stop "$UNIT" 2>/dev/null || true - [[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true - mv "$ROOT" "$FAILED_DIR" - cp -a "$BACKUP_DIR" "$ROOT" - systemctl start "$UNIT" || true - [[ "$APP" == "tesm" ]] && systemctl start "$MONITOR_UNIT" || true + # Dieselbe Funktion wie die EXIT-Falle -- zwei Wege, die dasselbe tun + # sollen, laufen sonst irgendwann auseinander. + zurueckrollen_install fail "Aktualisierung fehlgeschlagen. Alter Stand wiederhergestellt. Fehlgeschlagene Version : $FAILED_DIR Sicherung : $BACKUP_DIR @@ -568,12 +670,45 @@ if [[ $HTTPS -eq 1 ]]; then fi fi +# -- Lizenzserver einrichten ------------------------------------------------- +# Bei einer Neuinstallation: Signaturschluessel anlegen (braucht jeder +# Lizenzserver, um Kundenlizenzen zu signieren) und in der Master-Ausgabe +# gleich die eigene Master-Lizenz. Bei einer Aktualisierung nichts davon -- +# ein bestehender Schluessel darf nie angetastet werden. +if [[ "$APP" == "tesm-license" && $IS_UPDATE -eq 0 ]]; then + # Das Datenverzeichnis steht in der Umgebungsdatei -- es muss nicht + # $ROOT/data sein, wenn jemand es dort umgestellt hat. + DATEN_DIR="$(sed -n "s/^${ENV_PREFIX}DATA_DIR=//p" "$ENV_FILE" | tail -1)" + DATEN_DIR="${DATEN_DIR:-$ROOT/data}" + step "Signaturschluessel" + if sudo -u "$SERVICE_USER" "/usr/local/bin/$NAME-admin" init-key; then + info "Bitte die Datei $DATEN_DIR/master_signing_key.json SOFORT sichern." + else + fail "Der Signaturschluessel liess sich nicht erzeugen. Ohne ihn kann dieser Server + keine Lizenz ausstellen. Protokoll: journalctl -u $UNIT -n 50" + fi + + if [[ "$EDITION" == "master" ]]; then + step "Eigene Master-Lizenz" + if [[ -s "$DATEN_DIR/license.json" ]]; then + info "Es liegt schon eine Lizenzdatei -- unangetastet." + elif sudo -u "$SERVICE_USER" "/usr/local/bin/$NAME-admin" bootstrap; then + systemctl restart "$UNIT" + info "Dienst neu gestartet, damit die Lizenz gelesen wird." + else + fail "Die Master-Lizenz liess sich nicht ausstellen. + Nachtraeglich: sudo -u $SERVICE_USER $NAME-admin bootstrap" + fi + fi +fi + if [[ $IS_UPDATE -eq 1 ]]; then rm -rf "$BACKUP_DIR" info "Sicherung entfernt (Aktualisierung erfolgreich)" fi # -- Abschluss --------------------------------------------------------------- +INSTALL_FERTIG=1 step "Fertig" ADMIN_CMD="tesm-admin" [[ "$APP" == "tesm-license" ]] && ADMIN_CMD="tesm-license-admin" @@ -589,14 +724,24 @@ cat < Master-Ausgabe erkannt -- Release aus $REPO." + fi +fi + # --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. Setzt: $TAG (aufgeloest) und $SRC (entpacktes -# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf. +# 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 @@ -48,6 +77,15 @@ EXTRA=("$@") 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 -- @@ -63,11 +101,24 @@ hat_crlf() { } BOOTSTRAP_WORK="" -bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$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:-$APP}" asset + 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 @@ -78,9 +129,11 @@ release_holen() { if [[ "$TAG" == "latest" ]]; then echo "==> Ermittle aktuellen Release-Tag ..." - TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \ + 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 } @@ -89,7 +142,7 @@ release_holen() { asset="$repo-$TAG.tar.gz" echo "==> Lade $asset ..." - curl -fsSL -o "$BOOTSTRAP_WORK/$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 diff --git a/deploy/upgrade.sh b/deploy/upgrade.sh index 92fd894..d1864e6 100644 --- a/deploy/upgrade.sh +++ b/deploy/upgrade.sh @@ -53,6 +53,11 @@ 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. @@ -252,6 +257,20 @@ zurueckrollen() { 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)" @@ -286,8 +305,13 @@ fi # 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. Setzt: $TAG (aufgeloest) und $SRC (entpacktes -# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf. +# 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 @@ -297,6 +321,15 @@ fi 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 -- @@ -312,11 +345,24 @@ hat_crlf() { } BOOTSTRAP_WORK="" -bootstrap_aufraeumen() { [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$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:-$APP}" asset + 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 @@ -327,9 +373,11 @@ release_holen() { if [[ "$TAG" == "latest" ]]; then echo "==> Ermittle aktuellen Release-Tag ..." - TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \ + 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 } @@ -338,7 +386,7 @@ release_holen() { asset="$repo-$TAG.tar.gz" echo "==> Lade $asset ..." - curl -fsSL -o "$BOOTSTRAP_WORK/$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 @@ -514,6 +562,7 @@ for unit in "${ALTE_UNITS[@]}"; do 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 @@ -532,15 +581,12 @@ fi step "Neu installieren" INSTALL_ARGS=(--app "$APP" --yes) [[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE") -if ! bash "$SRC/deploy/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}; then - # Hier stand das System bisher halb umgebaut da: alter Dienst aus, neuer - # nicht vorhanden. Der Rueckweg bringt es in den Zustand von vorher, damit - # ein zweiter Versuch nach der Ursache moeglich ist. - zurueckrollen "$BACKUP" "Die Installation ist fehlgeschlagen." - fail "Upgrade abgebrochen und zurueckgerollt. Ursache oben nachsehen, dann erneut versuchen." -fi +# 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 @@ -592,6 +638,7 @@ else fi # -- Abschluss --------------------------------------------------------------- # +UPGRADE_FERTIG=1 step "Fertig" info "Sicherung der alten Installation : $ALT_KOPIE" if [[ "$APP" == "tesm" ]]; then diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..9aa0c37 --- /dev/null +++ b/install.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# ============================================================================= +# Installation -- der kurze Weg. +# +# Diese Datei liegt oben im Paket und ruft den Installer unter deploy/ auf: +# +# tar xzf tesm-vX.Y.Z.tar.gz && cd tesm +# sudo bash install.sh --app tesm --https +# +# Sie genuegt aber auch **allein**: liegt kein Paket um sie herum, holt sie +# sich das aktuelle Release selbst. +# +# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/install.sh +# sudo bash install.sh --app tesm --https +# +# Die Master-Ausgabe des Lizenzservers liegt im privaten Repositorium +# tesm-license-master. Allein-Betrieb dafuer: +# +# export TESM_RELEASE_REPO=tesm-license-master +# export TESM_RELEASE_TOKEN= +# sudo -E bash install.sh --app tesm-license --https +# +# Aus einem entpackten Paket braucht es das nicht: dort entscheidet, welches +# Paket es ist. Die Master-Ausgabe legt beim Installieren Signaturschluessel +# und eigene Master-Lizenz selbst an. +# +# Alle Argumente gehen unveraendert an deploy/install.sh weiter; dessen Hilfe: +# +# bash install.sh --help +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --app wird nur gelesen, nicht verbraucht: der Bootstrap braucht es fuer den +# Namen des Repositoriums, der Installer bekommt die Argumente unveraendert. +APP="" +TAG="latest" +ARGUMENTE=("$@") +for ((i = 0; i < $#; i++)); do + case "${ARGUMENTE[i]}" in + --app) APP="${ARGUMENTE[i + 1]:-}" ;; + --tag) TAG="${ARGUMENTE[i + 1]:-latest}" ;; + esac +done + +# Ohne Paket drumherum brauchen wir den Namen, um das richtige Release zu +# finden -- die beiden Anwendungen liegen in getrennten Repositorien. +if [[ ! -f "$SCRIPT_DIR/deploy/install.sh" ]]; then + [[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { + echo "Diese Datei liegt allein hier und muss sich das Release holen --" >&2 + echo "dafuer braucht sie --app ." >&2 + echo "Verwendung: install.sh --app [weitere Optionen]" >&2 + exit 2 + } +fi + +# --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 ------------------------------------------------------ + +if [[ -f "$SCRIPT_DIR/deploy/install.sh" ]]; then + # Im Paket: der Installer liegt daneben. + exec bash "$SCRIPT_DIR/deploy/install.sh" ${ARGUMENTE[0]+"${ARGUMENTE[@]}"} +fi + +# Allein: Release holen und dessen Installer benutzen. Kein exec -- sonst +# laeuft die Aufraeumfalle nie und das entpackte Release bliebe unter /tmp. +release_holen || { echo "Das Release liess sich nicht holen." >&2; exit 1; } +bash "$SRC/deploy/install.sh" ${ARGUMENTE[0]+"${ARGUMENTE[@]}"}