Compare commits

...
4 Commits
Author SHA1 Message Date
alientim cb056b4872 release.sh nimmt den Namen des Repositoriums 2026-09-09 00:13:00 +02:00
alientim ed84e8343e Version 2.1.0 2026-09-09 00:10:40 +02:00
alientim 157a244900 Ausgabe fuer Sub-Lizenzserver: kein Weg zur Rolle master 2026-09-09 00:07:34 +02:00
alientimandClaude Opus 5 fd333f60ff sudoers ohne requiretty, und ein Rueckweg fuer fehlgeschlagene Upgrades
Auf einem frisch aufgesetzten Server brach das Upgrade mitten drin ab:

    /tmp/tmp.XXXX:8:24: syntax error: unknown setting: 'requiretty'
    visudo: invalid sudoers file
     !! sudoers-Datei ist fehlerhaft

"Defaults:<user> !requiretty" stand in beiden sudoers-Dateien. Auf Debian und
Ubuntu ist requiretty nie gesetzt -- die Zeile war von Anfang an wirkungslos --
und ab sudo 1.9.16 kennt visudo die Einstellung nicht mehr und lehnt die ganze
Datei ab. Sie ist entfernt; ein Hygienetest haelt fest, dass sie nicht
zurueckkommt, und prueft gleich mit, dass in diesen Dateien genau eine Regel
steht und die auf den Helfer zeigt.

Schlimmer als der Fehler war die Folge: der alte Dienst war zu diesem Zeitpunkt
gestoppt und deaktiviert, das Verzeichnis teils ersetzt, der neue Dienst nicht
vorhanden. upgbrade.sh faengt den Fehlschlag jetzt ab und dreht den Lauf
zurueck -- und mit "--rollback" auch spaeter noch, aus einer vorhandenen
Sicherung.

Dafuer muss die Sicherung mehr enthalten als das Verzeichnis. Bei einem Upgrade
in dasselbe Verzeichnis schreibt install.sh auch systemd-Unit, nginx-Site,
logrotate und Banner neu und ersetzt das venv; ohne Kopie davon liesse sich der
alte Stand nicht herstellen. Ein Manifest haelt Quelle und Art des Umzugs fest,
damit ein spaeterer Rueckweg nicht raten muss.

Beim Nachstellen des Fehlschlags fiel ein zweiter Fund auf, gefaehrlicher als
der erste: die Liste der "alten Units" war fest verdrahtet -- fuer --app tesm
immer tesm.service. Nach einem rename.sh heisst so aber die *laufende*
Installation. Wer die archivierte 1.x in eine eigene Instanz hochzieht und
--keep-old-running vergisst, haette damit den Produktionsdienst gestoppt und
deaktiviert. Die Units werden jetzt daran erkannt, dass sie in genau das
Quellverzeichnis zeigen; Timer und Hilfsdienste kommen ueber den Namen der so
gefundenen Unit mit.

Am System geprueft, nicht behauptet: Fehlschlag nachgestellt (1.2.8 mit eigener
Unit, eigener Site und erkennbarem venv), Rueckweg lief -- VERSION 1.2.8, das
alte venv und die alte Unit wieder da, die neue Struktur weg, die laufende
Produktion unberuehrt. Danach derselbe Lauf mit heiler sudoers-Datei bis zum
Ende (4 Clients, 2 Zugangsdaten, DHCP), und zuletzt "--rollback" als eigener
Aufruf auf dieselbe Installation.

Version 2.0.10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:13:45 +02:00
18 changed files with 823 additions and 150 deletions

No files matched your search

+23 -8
View File
@@ -4,6 +4,13 @@ Ausgabe und Verwaltung der Lizenzen für TESM: Kunde -> Ticket -> Lizenz,
Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand über Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand über
MS Graph und eine Hierarchie aus Unter-Lizenzservern. 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 **Dieses Repository enthält genau das, was zum Installieren und Betreiben des
Lizenzservers gebraucht wird** -- nichts darüber hinaus: Lizenzservers gebraucht wird** -- nichts darüber hinaus:
@@ -21,6 +28,7 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin.
| Wo sonst noch | | | Wo sonst noch | |
|---|---| |---|---|
| [alientim/tesm](https://gitea.int.eertmoed.net/alientim/tesm) | TESM selbst | | [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 | | [alientim/TESM-DEV](https://gitea.int.eertmoed.net/alientim/TESM-DEV) | Entwicklung: beide Anwendungen, Tests, Architekturdoku |
## Zweige ## Zweige
@@ -34,16 +42,24 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin.
## Installation ## Installation
Eine einzelne Datei genügt -- sie lädt das Release selbst:
```bash ```bash
sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.de curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm-license/raw/branch/main/install.sh
sudo -u tesm-license tesm-license-admin init-key sudo bash install.sh --app tesm-license --https --domain lizenz.firma.de
sudo -u tesm-license tesm-license-admin bootstrap
sudo -u tesm-license tesm-license-admin create-admin sudo -u tesm-license tesm-license-admin create-admin
``` ```
**`init-key` erzeugt den Signaturschlüssel dieses Servers. Sichern Sie ihn Aus einem entpackten Paket ist es derselbe Aufruf ohne `curl`.
sofort.** Ohne ihn lässt sich keine ausgestellte Lizenz mehr erneuern, und
Kunden müssten einen Ausstellerwechsel bestätigen. 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 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. 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 ### Aktualisieren
```bash ```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 Vor jedem Update entsteht ein vollständiges Backup, danach läuft ein
@@ -70,7 +86,6 @@ Health-Check mit automatischem Rollback.
```bash ```bash
sudo -u tesm-license tesm-license-admin status 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 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 create-admin
sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand übernehmen sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand übernehmen
``` ```
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-license" name = "tesm-license"
version = "2.0.9" version = "2.1.0"
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -8,6 +8,7 @@ musste (und ``licensing.py`` byte-identisch gehalten werden *musste*).
from __future__ import annotations from __future__ import annotations
import importlib.util
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -51,6 +52,18 @@ ENV_PREFIX = "TESM_LICENSE_"
logger = logging.getLogger(__name__) 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: def master_key_path(config: Any) -> Path:
return Path(config.data_dir) / "master_signing_key.json" return Path(config.data_dir) / "master_signing_key.json"
@@ -32,6 +32,7 @@ from tesm_core.licensing_client import LICENSED_FEATURES, LicenseError
import tesm_licensing as lic import tesm_licensing as lic
from .. import is_master_edition
from ..services import master_key from ..services import master_key
from .common import can_issue_license_server_licenses, log_event, signing_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"]), signing_key_health=master_key.health(extension.registry["master_key_path"]),
may_issue_license_server=can_issue_license_server_licenses(), may_issue_license_server=can_issue_license_server_licenses(),
master_edition=is_master_edition(),
grace_days=lic.GRACE_PERIOD_DAYS, grace_days=lic.GRACE_PERIOD_DAYS,
) )
+35 -74
View File
@@ -1,13 +1,14 @@
"""Kommandozeilenwerkzeuge des Lizenzservers. """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 ``bootstrap`` stellt die eigene Lizenz der Wurzelinstanz aus und ist damit der
Master-Lizenz ist die einzige Lizenz, fuer die es **keine hoehere Instanz gibt**, einzige Weg, auf dem eine Lizenzdatei die Rolle ``master`` bekommt (ueber
die sie bestaetigen koennte. Ein Aktivierungs-Handshake gegen sich selbst waere Tickets ist sie auf jedem Server abgewiesen). Die Arbeit dazu liegt im
Theater. Das Skript signiert die Datei mit dem eigenen Schluessel und setzt den Teilpaket ``tesm_license.master``, das nur die Master-Ausgabe mitbringt; fehlt
Aktivierungszustand direkt -- und nur so kann eine Lizenzdatei je die Rolle es, sagt der Befehl das und tut nichts. Siehe dort fuer die Begruendung.
``master`` tragen. Die Ticket-Oberflaeche bietet diese Rolle an keiner Stelle an.
""" """
from __future__ import annotations from __future__ import annotations
@@ -20,7 +21,6 @@ from pathlib import Path
from typing import Any from typing import Any
import tesm_licensing as lic import tesm_licensing as lic
import tesm_licensing_server as licsrv
from tesm_core import audit from tesm_core import audit
from tesm_core.cli_web import add_web_setup, run_web_setup from tesm_core.cli_web import add_web_setup, run_web_setup
from tesm_core.db import Database 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.rbac import service as rbac
from tesm_core.security import passwords 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 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: 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() app, extension = _app_and_core()
key = master_key.ensure(master_key_path(extension.config)) document, state = eigene_lizenz_ausstellen(
app, extension, days=int(args.days), name=args.name
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
print(f"Master-Lizenz ausgestellt: {document['license_id']}") print(f"Master-Lizenz ausgestellt: {document['license_id']}")
print(f"Gueltig bis: {document['expires_at']}") 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: if state is not None:
print(f"Zustand: gueltig={state.valid}, aktiviert={state.activated}, Rolle={state.role}") print(f"Zustand: gueltig={state.valid}, aktiviert={state.activated}, Rolle={state.role}")
return 0 return 0
@@ -434,7 +390,12 @@ def build_parser() -> argparse.ArgumentParser:
init_key.set_defaults(func=cmd_init_key) init_key.set_defaults(func=cmd_init_key)
bootstrap = sub.add_parser( 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("--days", type=int, default=3650)
bootstrap.add_argument("--name", default="Lizenzserver (Eigenbetrieb)") bootstrap.add_argument("--name", default="Lizenzserver (Eigenbetrieb)")
@@ -116,9 +116,16 @@
{% endif %} {% endif %}
</section> </section>
{% else %} {% else %}
{{ empty_state('Keine gültige Lizenz', {% if master_edition %}
'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap" {{ empty_state('Keine gültige Lizenz',
die eigene Lizenz der Wurzelinstanz.', 'key') }} '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 %} {% endif %}
{% if pending %} {% if pending %}
+165 -20
View File
@@ -28,6 +28,9 @@ set -euo pipefail
APP="" APP=""
INSTANCE="" INSTANCE=""
#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die
#: Portsuche in einem Test wirklich ausfuehren laesst.
ETC_TESM="${TESM_ETC_DIR:-/etc/tesm}"
PORT="" PORT=""
HTTP_PORT="" HTTP_PORT=""
SKIP_PACKAGES=0 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; } fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
info() { printf ' %s\n' "$*"; } 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() { usage() {
cat <<'USAGE' cat <<'USAGE'
Verwendung: install.sh --app <tesm|tesm-license> [Optionen] Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
@@ -112,13 +171,24 @@ MONITOR_UNIT="$NAME-monitor.service"
NGINX_SITE="/etc/nginx/sites-available/$NAME" NGINX_SITE="/etc/nginx/sites-available/$NAME"
ENV_PREFIX="TESM_" ENV_PREFIX="TESM_"
[[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_" [[ "$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 # Bei einer Aktualisierung gilt zuerst, was schon eingerichtet ist. Sonst
# verschiebt ein Update ohne Portangaben die Anwendung: eine benannte Instanz # verschiebt ein Update ohne Portangaben die Anwendung: eine benannte Instanz
# faellt auf 8080/8443 zurueck, auch wenn sie seit der Installation auf Port 80 # 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 # lief -- und ein Reverse Proxy davor zeigt dann ins Leere. Dieselbe Regel wie
# beim Secure-Flag weiter unten: eine Aktualisierung nimmt nichts weg, was # beim Secure-Flag weiter unten: eine Aktualisierung nimmt nichts weg, was
# nicht ausdruecklich neu gesetzt wird. # nicht ausdruecklich neu gesetzt wird.
ENV_FILE="/etc/tesm/$NAME.env" ENV_FILE="$ETC_TESM/$NAME.env"
if [[ -z "$PORT" && -f "$ENV_FILE" ]]; then 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)" PORT="$(sed -n "s/^${ENV_PREFIX}BIND=127\.0\.0\.1:\([0-9]\{1,5\}\)$/\1/p" "$ENV_FILE" | head -1)"
fi 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)" [[ -z "$HTTPS_PORT" ]] && HTTPS_PORT="$(sed -n 's/^[[:space:]]*listen[[:space:]]\([0-9]\{1,5\}\)[[:space:]]\+ssl.*/\1/p' "$NGINX_SITE" | head -1)"
fi 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 if [[ -z "$PORT" ]]; then
PORT=5000 BASISPORT=5000
[[ "$APP" == "tesm-license" ]] && PORT=5001 [[ "$APP" == "tesm-license" ]] && BASISPORT=5001
[[ -n "$INSTANCE" ]] && PORT=$((PORT + 100)) 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 fi
if [[ -z "$HTTP_PORT" ]]; then if [[ -z "$HTTP_PORT" ]]; then
HTTP_PORT=80 HTTP_PORT=80
@@ -212,12 +302,24 @@ fi
# Der Vorgaenger lief als root. Hier laeuft der Dienst unprivilegiert; Rootrechte # Der Vorgaenger lief als root. Hier laeuft der Dienst unprivilegiert; Rootrechte
# gibt es ausschliesslich ueber den Helfer. # 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 ------------------------------------------------------------------ # -- Backup ------------------------------------------------------------------
if [[ $IS_UPDATE -eq 1 ]]; then if [[ $IS_UPDATE -eq 1 ]]; then
step "Sicherung vor der Aktualisierung" step "Sicherung vor der Aktualisierung"
systemctl stop "$UNIT" 2>/dev/null || true systemctl stop "$UNIT" 2>/dev/null || true
[[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true [[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true
cp -a "$ROOT" "$BACKUP_DIR" cp -a "$ROOT" "$BACKUP_DIR"
SICHERUNG_STEHT=1
info "Sicherung: $BACKUP_DIR" info "Sicherung: $BACKUP_DIR"
fi fi
@@ -347,8 +449,8 @@ step "Umgebung"
# EnvironmentFile, die Kommandozeilenwerkzeuge ueber ihren Wrapper. Ohne das # EnvironmentFile, die Kommandozeilenwerkzeuge ueber ihren Wrapper. Ohne das
# wuesste "tesm-admin" nicht, wo die Instanz liegt, und wuerde sich eine leere # wuesste "tesm-admin" nicht, wo die Instanz liegt, und wuerde sich eine leere
# Datenbank im Arbeitsverzeichnis anlegen. # Datenbank im Arbeitsverzeichnis anlegen.
install -d -o root -g root -m 0755 /etc/tesm install -d -o root -g root -m 0755 "$ETC_TESM"
ENV_FILE="/etc/tesm/$NAME.env" ENV_FILE="$ETC_TESM/$NAME.env"
TMP_ENV="$(mktemp)" TMP_ENV="$(mktemp)"
{ {
echo "# Von install.sh erzeugt fuer $NAME -- nicht von Hand bearbeiten." echo "# Von install.sh erzeugt fuer $NAME -- nicht von Hand bearbeiten."
@@ -491,7 +593,11 @@ fi
step "Gesundheitspruefung" step "Gesundheitspruefung"
HEALTHY=0 HEALTHY=0
for attempt in $(seq 1 20); do 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 HEALTHY=1
info "Antwort nach ${attempt}x" info "Antwort nach ${attempt}x"
break break
@@ -502,13 +608,9 @@ done
if [[ $HEALTHY -eq 0 ]]; then if [[ $HEALTHY -eq 0 ]]; then
warn "Die Anwendung antwortet nicht." warn "Die Anwendung antwortet nicht."
if [[ $IS_UPDATE -eq 1 ]]; then if [[ $IS_UPDATE -eq 1 ]]; then
warn "Rollback wird ausgefuehrt." # Dieselbe Funktion wie die EXIT-Falle -- zwei Wege, die dasselbe tun
systemctl stop "$UNIT" 2>/dev/null || true # sollen, laufen sonst irgendwann auseinander.
[[ "$APP" == "tesm" ]] && systemctl stop "$MONITOR_UNIT" 2>/dev/null || true zurueckrollen_install
mv "$ROOT" "$FAILED_DIR"
cp -a "$BACKUP_DIR" "$ROOT"
systemctl start "$UNIT" || true
[[ "$APP" == "tesm" ]] && systemctl start "$MONITOR_UNIT" || true
fail "Aktualisierung fehlgeschlagen. Alter Stand wiederhergestellt. fail "Aktualisierung fehlgeschlagen. Alter Stand wiederhergestellt.
Fehlgeschlagene Version : $FAILED_DIR Fehlgeschlagene Version : $FAILED_DIR
Sicherung : $BACKUP_DIR Sicherung : $BACKUP_DIR
@@ -568,12 +670,45 @@ if [[ $HTTPS -eq 1 ]]; then
fi fi
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 if [[ $IS_UPDATE -eq 1 ]]; then
rm -rf "$BACKUP_DIR" rm -rf "$BACKUP_DIR"
info "Sicherung entfernt (Aktualisierung erfolgreich)" info "Sicherung entfernt (Aktualisierung erfolgreich)"
fi fi
# -- Abschluss --------------------------------------------------------------- # -- Abschluss ---------------------------------------------------------------
INSTALL_FERTIG=1
step "Fertig" step "Fertig"
ADMIN_CMD="tesm-admin" ADMIN_CMD="tesm-admin"
[[ "$APP" == "tesm-license" ]] && ADMIN_CMD="tesm-license-admin" [[ "$APP" == "tesm-license" ]] && ADMIN_CMD="tesm-license-admin"
@@ -589,14 +724,24 @@ cat <<SUMMARY
Naechste Schritte: Naechste Schritte:
SUMMARY SUMMARY
if [[ "$APP" == "tesm-license" ]]; then if [[ "$APP" == "tesm-license" && "$EDITION" == "master" ]]; then
cat <<SUMMARY cat <<SUMMARY
1. Signaturschluessel erzeugen und sofort sichern: 1. Administrator anlegen:
sudo -u $SERVICE_USER $NAME-admin init-key
2. Eigene Master-Lizenz ausstellen:
sudo -u $SERVICE_USER $NAME-admin bootstrap
3. Administrator anlegen:
sudo -u $SERVICE_USER $NAME-admin create-admin sudo -u $SERVICE_USER $NAME-admin create-admin
2. Signaturschluessel sichern -- ohne ihn ist keine ausgestellte Lizenz
mehr pruefbar:
$ROOT/data/master_signing_key.json
SUMMARY
elif [[ "$APP" == "tesm-license" ]]; then
cat <<SUMMARY
1. Administrator anlegen:
sudo -u $SERVICE_USER $NAME-admin create-admin
2. Beim Master-Lizenzserver eine Lizenz fuer diesen Server anfordern und
unter "Eigene Lizenz" einspielen. Diese Ausgabe ist ein Sub-Lizenzserver
und stellt keine eigene Master-Lizenz aus.
3. Signaturschluessel sichern -- mit ihm signiert dieser Server seine
Kundenlizenzen:
$ROOT/data/master_signing_key.json
SUMMARY SUMMARY
else else
cat <<SUMMARY cat <<SUMMARY
+12 -3
View File
@@ -14,12 +14,21 @@ TAG="${1:?Verwendung: release.sh vX.Y.Z [name]}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Vorgabe: der Name der einzigen Anwendung im Baum, sonst der Verzeichnisname. # Vorgabe: der Name des Repositoriums. Genau den erwartet der Bootstrap von
# update.sh als Anhangsnamen ("$repo-$TAG.tar.gz"). Die Anwendung heisst nicht
# immer so: im Repositorium tesm-license-master liegt die Anwendung
# "tesm-license", und ein Anhang unter deren Namen waere nie zu finden.
if [[ -n "${2:-}" ]]; then if [[ -n "${2:-}" ]]; then
NAME="$2" NAME="$2"
else else
mapfile -t APPS < <(ls -1 "$REPO_DIR/apps" 2>/dev/null || true) HERKUNFT="$(git -C "$REPO_DIR" config --get remote.origin.url 2>/dev/null || true)"
if [[ ${#APPS[@]} -eq 1 ]]; then NAME="${APPS[0]}"; else NAME="$(basename "$REPO_DIR")"; fi if [[ -n "$HERKUNFT" ]]; then
NAME="$(basename "$HERKUNFT" .git)"
else
# Ohne Gegenseite: die einzige Anwendung im Baum, sonst das Verzeichnis.
mapfile -t APPS < <(ls -1 "$REPO_DIR/apps" 2>/dev/null || true)
if [[ ${#APPS[@]} -eq 1 ]]; then NAME="${APPS[0]}"; else NAME="$(basename "$REPO_DIR")"; fi
fi
fi fi
OUT="$NAME-$TAG.tar.gz" OUT="$NAME-$TAG.tar.gz"
+7 -1
View File
@@ -5,5 +5,11 @@
# #
# Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe # Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe
# Problem, indem die gesamte Anwendung als root lief. # Problem, indem die gesamte Anwendung als root lief.
Defaults:tesm !requiretty #
# Hier stand "Defaults:tesm !requiretty". Auf Debian und Ubuntu ist
# requiretty nie gesetzt, die Zeile war also von Anfang an wirkungslos -- und
# ab sudo 1.9.16 kennt visudo die Einstellung nicht mehr und lehnt die ganze
# Datei ab ("unknown setting: 'requiretty'"). Auf einem frisch aufgesetzten
# Server brach die Installation deshalb mitten drin ab. Nicht wieder
# hinzufuegen; ein Hygienetest haelt das fest.
tesm ALL=(root) NOPASSWD: /usr/local/lib/tesm/tesm-helper tesm ALL=(root) NOPASSWD: /usr/local/lib/tesm/tesm-helper
+7 -1
View File
@@ -5,5 +5,11 @@
# #
# Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe # Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe
# Problem, indem die gesamte Anwendung als root lief. # Problem, indem die gesamte Anwendung als root lief.
Defaults:tesm-license !requiretty #
# Hier stand "Defaults:tesm-license !requiretty". Auf Debian und Ubuntu ist
# requiretty nie gesetzt, die Zeile war also von Anfang an wirkungslos -- und
# ab sudo 1.9.16 kennt visudo die Einstellung nicht mehr und lehnt die ganze
# Datei ab ("unknown setting: 'requiretty'"). Auf einem frisch aufgesetzten
# Server brach die Installation deshalb mitten drin ab. Nicht wieder
# hinzufuegen; ein Hygienetest haelt das fest.
tesm-license ALL=(root) NOPASSWD: /usr/local/lib/tesm/tesm-helper tesm-license ALL=(root) NOPASSWD: /usr/local/lib/tesm/tesm-helper
+59 -6
View File
@@ -11,6 +11,10 @@
# Auf einer bestehenden Installation liegt sie schon bereit: # Auf einer bestehenden Installation liegt sie schon bereit:
# #
# sudo bash /srv/tesm/deploy/update.sh tesm # sudo bash /srv/tesm/deploy/update.sh tesm
#
# Laeuft hier die Master-Ausgabe des Lizenzservers, kommt das Release aus dem
# privaten Repositorium tesm-license-master -- dafuer braucht es einen
# Zugriffstoken in TESM_RELEASE_TOKEN oder in /etc/tesm-release.token.
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
@@ -31,14 +35,39 @@ EXTRA=("$@")
exit 2 exit 2
} }
# Repositorium: die Master-Ausgabe des Lizenzservers liegt in einem eigenen,
# privaten Repositorium, heisst als Installation aber weiter "tesm-license".
# Erkannt wird sie an der Installation und nicht an einem Schalter -- wer
# aktualisiert, soll nicht erst wissen muessen, welche Ausgabe hier laeuft.
# Sonst holte der Aufruf die Ausgabe fuer Sub-Lizenzserver, und der Installer
# muesste sie abweisen.
REPO="$APP"
if [[ "$APP" == "tesm-license" ]]; then
NAME="$APP"
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
[[ "${EXTRA[i]}" == "--instance" ]] && NAME="$APP-${EXTRA[i + 1]:-}"
done
# Die Wurzel ist ueberschreibbar, damit ein Test die Erkennung wirklich
# ausfuehren kann und nicht nur den Quelltext liest.
if [[ -d "${TESM_SRV_ROOT:-/srv}/$NAME/apps/tesm-license/src/tesm_license/master" ]]; then
REPO="tesm-license-master"
echo "==> Master-Ausgabe erkannt -- Release aus $REPO."
fi
fi
# --8<-- Bootstrap: Release holen -------------------------------------------- # --8<-- Bootstrap: Release holen --------------------------------------------
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden # Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und # Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen, # beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt. # ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
# #
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes # Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf. # (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: # Zwei Fallen des Vorgaengers sind hier vermieden:
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die # * 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}" BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
OWNER="${TESM_RELEASE_OWNER:-alientim}" 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 # Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem # ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt -- # Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
@@ -63,11 +101,24 @@ hat_crlf() {
} }
BOOTSTRAP_WORK="" 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 trap bootstrap_aufraeumen EXIT
release_holen() { 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 for werkzeug in curl tar python3 find; do
command -v "$werkzeug" >/dev/null 2>&1 || { command -v "$werkzeug" >/dev/null 2>&1 || {
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2 echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
@@ -78,9 +129,11 @@ release_holen() {
if [[ "$TAG" == "latest" ]]; then if [[ "$TAG" == "latest" ]]; then
echo "==> Ermittle aktuellen Release-Tag ..." 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"])')" || { | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2 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 echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
return 1 return 1
} }
@@ -89,7 +142,7 @@ release_holen() {
asset="$repo-$TAG.tar.gz" asset="$repo-$TAG.tar.gz"
echo "==> Lade $asset ..." 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" || { "$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2 echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
return 1 return 1
+285 -28
View File
@@ -52,6 +52,12 @@ WITH_DATA=1
STOP_OLD=1 STOP_OLD=1
ADOPT_KEY=1 ADOPT_KEY=1
TAG="latest" 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 #: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der
#: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein #: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein
#: Hygienetest vergleicht sie. #: Hygienetest vergleicht sie.
@@ -95,6 +101,10 @@ Verwendung: upgrade.sh --app <tesm|tesm-license> [Optionen]
Vorgabe ist deshalb die Uebernahme. Vorgabe ist deshalb die Uebernahme.
--tag <vX.Y.Z> Bestimmte Version installieren (Vorgabe: latest). --tag <vX.Y.Z> Bestimmte Version installieren (Vorgabe: latest).
Wird nur beachtet, wenn das Release geladen wird. 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 --dry-run Nur zeigen, was geschehen wuerde
--yes Keine Rueckfrage --yes Keine Rueckfrage
-h, --help Diese Hilfe -h, --help Diese Hilfe
@@ -122,6 +132,11 @@ while [[ $# -gt 0 ]]; do
--new-key) ADOPT_KEY=0; shift ;; --new-key) ADOPT_KEY=0; shift ;;
--adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit --adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit
--tag) TAG="${2:?}"; shift 2 ;; --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 ;; --dry-run) DRY_RUN=1; shift ;;
--yes|-y) ASSUME_YES=1; shift ;; --yes|-y) ASSUME_YES=1; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
@@ -132,14 +147,171 @@ done
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; } [[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren" [[ "$(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 -------------------------------------------- # --8<-- Bootstrap: Release holen --------------------------------------------
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden # Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und # Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen, # beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt. # ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
# #
# Erwartet: $APP und $TAG. Setzt: $TAG (aufgeloest) und $SRC (entpacktes # Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG
# Release). Raeumt sein Arbeitsverzeichnis beim Beenden auf. # (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: # Zwei Fallen des Vorgaengers sind hier vermieden:
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die # * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
@@ -149,6 +321,15 @@ done
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}" BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
OWNER="${TESM_RELEASE_OWNER:-alientim}" 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 # Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem # ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt -- # Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
@@ -164,11 +345,24 @@ hat_crlf() {
} }
BOOTSTRAP_WORK="" 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 trap bootstrap_aufraeumen EXIT
release_holen() { 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 for werkzeug in curl tar python3 find; do
command -v "$werkzeug" >/dev/null 2>&1 || { command -v "$werkzeug" >/dev/null 2>&1 || {
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2 echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
@@ -179,9 +373,11 @@ release_holen() {
if [[ "$TAG" == "latest" ]]; then if [[ "$TAG" == "latest" ]]; then
echo "==> Ermittle aktuellen Release-Tag ..." 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"])')" || { | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2 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 echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
return 1 return 1
} }
@@ -190,7 +386,7 @@ release_holen() {
asset="$repo-$TAG.tar.gz" asset="$repo-$TAG.tar.gz"
echo "==> Lade $asset ..." 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" || { "$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2 echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
return 1 return 1
@@ -255,28 +451,55 @@ fi
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)" [[ -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" 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 IN_PLACE=0
[[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1 [[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1
STAMP="$(date +%Y%m%d-%H%M%S)" # Die Units der alten Fassung erkennt man daran, dass sie in genau dieses
BACKUP="/srv/$NAME-upgrade-$STAMP" # Verzeichnis zeigen -- nicht am Namen der Anwendung. Nach einem rename.sh
ALT_KOPIE="$BACKUP/alt" # heisst die *laufende* Installation "tesm", und eine Namensliste haette sie
EXPORT_ALT="$BACKUP/export-alt.json" # gestoppt und deaktiviert, sobald jemand die archivierte 1.x in eine eigene
IMPORT_NEU="$BACKUP/import-neu.json" # 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[*]-}
}
# Alte Einheiten der Vorgaengerfassung. Der Timer gehoert dazu: nur den Dienst mapfile -t ALTE_UNITS < <(alte_units_finden "$SOURCE")
# zu stoppen liesse ihn beim naechsten Ablauf wieder anlaufen. if [[ ${#ALTE_UNITS[@]} -eq 0 ]]; then
if [[ "$APP" == "tesm" ]]; then if [[ "$SOURCE" == "/srv/$APP" ]]; then
ALTE_UNITS=(tesm.service tesm-check.timer tesm-check.service # Kein Fund, aber die Quelle ist der Hauptpfad: die historischen Namen der
tesm-check-restart.timer tesm-check-restart.service) # Fassung 1.x als Rueckfall.
else if [[ "$APP" == "tesm" ]]; then
ALTE_UNITS=(tesm-license.service) 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 fi
step "Vorhaben" step "Vorhaben"
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}" info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
info "Neue Fassung : $RELEASE_HERKUNFT" info "Neue Fassung : $RELEASE_HERKUNFT"
@@ -302,15 +525,45 @@ fi
# -- 1. Sichern -------------------------------------------------------------- # # -- 1. Sichern -------------------------------------------------------------- #
step "Sichern" step "Sichern"
mkdir -p "$ALT_KOPIE" mkdir -p "$ALT_KOPIE" "$SYSTEM_KOPIE"
chmod 700 "$BACKUP" 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, # Das venv kommt nur mit, wenn in dasselbe Verzeichnis installiert wird -- dort
# gross, und seine Shebangs zeigen ohnehin auf den alten Pfad. # ersetzt install.sh es, und ohne das alte startet der alte Dienst nach einem
tar -C "$SOURCE" --exclude=venv --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf - # Rueckweg nicht mehr. Bei einer eigenen Instanz bleibt die alte Installation
info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (ohne venv)" # 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" [[ -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 ---------------------------------------------- # # -- 2. Altes System abschalten ---------------------------------------------- #
if [[ $STOP_OLD -eq 1 ]]; then if [[ $STOP_OLD -eq 1 ]]; then
step "Altes System abschalten" step "Altes System abschalten"
@@ -328,9 +581,12 @@ fi
step "Neu installieren" step "Neu installieren"
INSTALL_ARGS=(--app "$APP" --yes) INSTALL_ARGS=(--app "$APP" --yes)
[[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE") [[ -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[@]}"} bash "$SRC/deploy/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
if [[ $WITH_DATA -eq 0 ]]; then if [[ $WITH_DATA -eq 0 ]]; then
UPGRADE_FERTIG=1
step "Fertig -- ohne Datenuebernahme" step "Fertig -- ohne Datenuebernahme"
info "Sicherung der alten Installation: $ALT_KOPIE" info "Sicherung der alten Installation: $ALT_KOPIE"
exit 0 exit 0
@@ -382,6 +638,7 @@ else
fi fi
# -- Abschluss --------------------------------------------------------------- # # -- Abschluss --------------------------------------------------------------- #
UPGRADE_FERTIG=1
step "Fertig" step "Fertig"
info "Sicherung der alten Installation : $ALT_KOPIE" info "Sicherung der alten Installation : $ALT_KOPIE"
if [[ "$APP" == "tesm" ]]; then if [[ "$APP" == "tesm" ]]; then
+18
View File
@@ -174,6 +174,24 @@ sudo bash deploy/upgrade.sh --app tesm-license
Eine bestimmte Version statt `latest`: `--tag v2.0.8`. Eine bestimmte Version statt `latest`: `--tag v2.0.8`.
**Wenn etwas schiefgeht.** Schlägt `install.sh` fehl, dreht `upgrade.sh` den
Lauf selbst zurück: der neue Dienst wird gestoppt, das Verzeichnis aus der
Sicherung wiederhergestellt und der alte Dienst wieder eingeschaltet. Das
Verzeichnis allein genügt dafür nicht -- bei einem Upgrade in dasselbe
Verzeichnis schreibt `install.sh` auch systemd-Unit, nginx-Site, logrotate und
Banner neu und ersetzt das venv. Die Sicherung enthält deshalb alles davon,
und ein Manifest hält fest, woher der Stand kam.
Später von Hand zurückdrehen, etwa nach einem Lauf, der durchlief aber nicht
gefallen hat:
```bash
sudo bash upgrade.sh --app tesm --rollback # neueste Sicherung
sudo bash upgrade.sh --app tesm --rollback /srv/tesm-upgrade-20260908-225428
```
Die Sicherung bleibt danach liegen; gelöscht wird nichts.
Ohne `--source` wird die alte Fassung an zwei Stellen gesucht: unter Ohne `--source` wird die alte Fassung an zwei Stellen gesucht: unter
`/srv/<app>` und unter `/srv/<app>-sonnet5`. Der zweite Ort ist der, an den `/srv/<app>` und unter `/srv/<app>-sonnet5`. Der zweite Ort ist der, an den
`rename.sh` sie verschiebt, sobald der Hauptname für die neue Installation `rename.sh` sie verschiebt, sobald der Hauptname für die neue Installation
+181
View File
@@ -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=<Gitea-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 <tesm|tesm-license>." >&2
echo "Verwendung: install.sh --app <tesm|tesm-license> [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 "<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 ------------------------------------------------------
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[@]}"}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.0.9" version = "2.1.0"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -134,7 +134,7 @@ def create_base_app(
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
#: Anwendungen. #: Anwendungen.
auth_tagline: str = "", auth_tagline: str = "",
app_version: str = "2.0.9", app_version: str = "2.1.0",
built_in_trusted_keys: tuple[str, ...] = (), built_in_trusted_keys: tuple[str, ...] = (),
enable_license: bool = True, enable_license: bool = True,
template_folder: str | None = None, template_folder: str | None = None,
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-licensing-server" name = "tesm-licensing-server"
version = "2.0.9" version = "2.1.0"
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten" description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["tesm-licensing==2.0.9", "cryptography>=41"] dependencies = ["tesm-licensing==2.1.0", "cryptography>=41"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-licensing" name = "tesm-licensing"
version = "2.0.9" version = "2.1.0"
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["cryptography>=42"] dependencies = ["cryptography>=42"]