Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
119293e5e3 | ||
|
|
36d9cb248e | ||
|
|
cb056b4872 | ||
|
|
ed84e8343e | ||
|
|
157a244900 | ||
|
|
fd333f60ff | ||
|
|
b9342d384c | ||
|
|
f062314a0a |
No files matched your search
@@ -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
|
||||
```
|
||||
@@ -135,3 +150,19 @@ Entwicklungsrepository, ebenso die Tests.
|
||||
* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen.
|
||||
* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet.
|
||||
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
|
||||
|
||||
---
|
||||
|
||||
## Von einer Installation der Fassung 1.x
|
||||
|
||||
Eine einzelne Datei genügt -- sie lädt das Release selbst:
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm-license/raw/branch/main/deploy/upgrade.sh
|
||||
sudo bash upgrade.sh --app tesm-license
|
||||
```
|
||||
|
||||
Sichert die alte Installation, schaltet sie ab, installiert diese Fassung und
|
||||
übernimmt die Daten. Einzelheiten und was **nicht** mitkommt:
|
||||
[docs/BETRIEB.md](docs/BETRIEB.md).
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-license"
|
||||
version = "2.0.7"
|
||||
version = "2.1.2"
|
||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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, state = eigene_lizenz_ausstellen(
|
||||
app, extension, days=int(args.days), name=args.name
|
||||
)
|
||||
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"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)")
|
||||
|
||||
@@ -116,9 +116,16 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
{% else %}
|
||||
{% 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 %}
|
||||
|
||||
+181
-20
@@ -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."
|
||||
# Nur sagen, was es wirklich gibt: bei einem Abbruch an der Rueckfrage
|
||||
# existiert das Verzeichnis noch nicht.
|
||||
if [[ -n "${ROOT:-}" && -d "${ROOT:-}" ]]; then
|
||||
warn "Angelegt wurde bis hierhin: $ROOT (Daten unter data/ unberuehrt)."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return "$code"
|
||||
}
|
||||
trap bei_fehler EXIT
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
||||
@@ -112,13 +171,27 @@ 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"
|
||||
# Wo die Daten liegen, steht in der Umgebungsdatei; ohne sie beim Vorgabeort.
|
||||
DATEN_DIR="$(sed -n "s/^${ENV_PREFIX}DATA_DIR=//p" "$ENV_FILE" 2>/dev/null | tail -1)"
|
||||
DATEN_DIR="${DATEN_DIR:-$ROOT/data}"
|
||||
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 +200,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 +305,41 @@ 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.
|
||||
# Die Rolle steht in der eingespielten Lizenz. Das ist die zuverlaessigere
|
||||
# Haelfte der Pruefung: eine Master-Installation aus einer aelteren Fassung
|
||||
# kennt das Teilpaket noch nicht, ist aber trotzdem eine Wurzelinstanz.
|
||||
ROLLE_ALT=""
|
||||
if [[ "$EDITION" == "sub" && -s "$DATEN_DIR/license.json" ]]; then
|
||||
ROLLE_ALT="$(python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print(json.load(open(sys.argv[1])).get("role", ""))
|
||||
except Exception:
|
||||
pass
|
||||
' "$DATEN_DIR/license.json" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ "$EDITION" == "sub" && ( -d "$ROOT/$MASTER_TEILPAKET" || "$ROLLE_ALT" == "master" ) ]]; then
|
||||
fail "Hier laeuft eine Wurzelinstanz${ROLLE_ALT:+ (Lizenz mit Rolle $ROLLE_ALT)}, 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: eine vom Master ausgestellte Lizenz einspielen (Rolle
|
||||
license_server) und $ROOT/$MASTER_TEILPAKET entfernen -- danach ist dies ein
|
||||
Sub-Lizenzserver und dieses Paket das richtige."
|
||||
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 +469,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 +613,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 +628,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 +690,41 @@ 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
|
||||
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 +740,24 @@ cat <<SUMMARY
|
||||
Naechste Schritte:
|
||||
SUMMARY
|
||||
|
||||
if [[ "$APP" == "tesm-license" ]]; then
|
||||
if [[ "$APP" == "tesm-license" && "$EDITION" == "master" ]]; then
|
||||
cat <<SUMMARY
|
||||
1. Signaturschluessel erzeugen und sofort sichern:
|
||||
sudo -u $SERVICE_USER $NAME-admin init-key
|
||||
2. Eigene Master-Lizenz ausstellen:
|
||||
sudo -u $SERVICE_USER $NAME-admin bootstrap
|
||||
3. Administrator anlegen:
|
||||
1. Administrator anlegen:
|
||||
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
|
||||
else
|
||||
cat <<SUMMARY
|
||||
|
||||
+10
-1
@@ -14,13 +14,22 @@ TAG="${1:?Verwendung: release.sh vX.Y.Z [name]}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && 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
|
||||
NAME="$2"
|
||||
else
|
||||
HERKUNFT="$(git -C "$REPO_DIR" config --get remote.origin.url 2>/dev/null || true)"
|
||||
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
|
||||
|
||||
OUT="$NAME-$TAG.tar.gz"
|
||||
git -c core.autocrlf=false -C "$REPO_DIR" archive --format=tar.gz --prefix="$NAME/" -o "$OUT" "$TAG"
|
||||
|
||||
+3
-3
@@ -3,8 +3,8 @@
|
||||
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
|
||||
# (oder umgekehrt).
|
||||
#
|
||||
# sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
||||
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus \
|
||||
# sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test \
|
||||
# --to tesm-license
|
||||
#
|
||||
# Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses
|
||||
@@ -51,7 +51,7 @@ usage() {
|
||||
Verwendung: rename.sh --app <tesm|tesm-license> --from <alt> --to <neu> [Optionen]
|
||||
|
||||
--app <name> Welche Anwendung (Pflicht)
|
||||
--from <name> Bisheriger Installationsname, z.B. tesm-opus (Pflicht)
|
||||
--from <name> Bisheriger Installationsname, z.B. tesm-test (Pflicht)
|
||||
--to <name> Neuer Installationsname, z.B. tesm (Pflicht)
|
||||
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
|
||||
der Fassung 1.x (Vorgabe: sonnet5)
|
||||
|
||||
+7
-1
@@ -5,5 +5,11 @@
|
||||
#
|
||||
# Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe
|
||||
# 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
|
||||
@@ -5,5 +5,11 @@
|
||||
#
|
||||
# Bewusst KEIN "ALL=(ALL) NOPASSWD: ALL". Der Vorgaenger loeste dasselbe
|
||||
# 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
|
||||
+1
-1
@@ -47,7 +47,7 @@ check_in_list() {
|
||||
}
|
||||
|
||||
# Name einer Installation auf diesem Host: entweder die Hauptinstallation
|
||||
# ("tesm") oder eine benannte Instanz daneben ("tesm-opus"). Mehr als diese
|
||||
# ("tesm") oder eine benannte Instanz daneben ("tesm-test"). Mehr als diese
|
||||
# beiden Formen gibt es nicht -- so kann keine erfundene Zeichenkette einen
|
||||
# Pfad ausserhalb des vorgesehenen Namensraums erzeugen.
|
||||
check_app_key() {
|
||||
|
||||
+154
-44
@@ -1,89 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Bootstrapper: laedt ein Release herunter und ruft dessen install.sh auf.
|
||||
# Aktualisierung: laedt das aktuelle Release und ruft dessen install.sh auf.
|
||||
# Enthaelt selbst keinerlei Installationslogik.
|
||||
#
|
||||
# Zwei Fallen des Vorgaengers sind hier bewusst 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-Assets.
|
||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert -- faengt
|
||||
# denselben Fehler ein zweites Mal ab.
|
||||
# Eine einzelne Datei genuegt -- sie holt sich alles Weitere:
|
||||
#
|
||||
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||
# sudo bash update.sh tesm
|
||||
#
|
||||
# Auf einer bestehenden Installation liegt sie schon bereit:
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
APP="${1:-}"
|
||||
TAG="${2:-latest}"
|
||||
|
||||
# Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse
|
||||
# sich eine benannte Instanz nicht aktualisieren: install.sh leitet den Namen
|
||||
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-opus" eine
|
||||
# aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-test" eine
|
||||
# zweite Installation namens "tesm" -- mit eigener Datenbank, eigener
|
||||
# nginx-Site und demselben Port.
|
||||
shift $(( $# > 2 ? 2 : $# ))
|
||||
EXTRA=("$@")
|
||||
# Bezugsquelle. Die Vorgaben zeigen dorthin, wo die Releases tatsaechlich
|
||||
# liegen -- ein Platzhalter wie "gitea.example.invalid" macht das Skript auf
|
||||
# jedem Host unbrauchbar, und genau das war hier der Fall.
|
||||
#
|
||||
# Das Repository heisst wie die Anwendung; getrennte Repositorien, getrennte
|
||||
# Releases. Der Anhang heisst "<repo>-<tag>.tar.gz", siehe unten.
|
||||
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||
|
||||
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || {
|
||||
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
||||
echo "Beispiel : update.sh tesm latest --instance opus" >&2
|
||||
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
||||
echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
REPO="${TESM_RELEASE_REPO:-$APP}"
|
||||
# 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
|
||||
|
||||
WORK="$(mktemp -d /tmp/tesm-update-XXXXXX)"
|
||||
trap 'rm -rf "$WORK"' EXIT # anders als im Vorgaenger wird aufgeraeumt
|
||||
# --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 "$BASE/api/v1/repos/$OWNER/$REPO/releases/latest" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
|
||||
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 -o "$WORK/$ASSET" "$BASE/$OWNER/$REPO/releases/download/$TAG/$ASSET"
|
||||
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 "$WORK/$ASSET" -C "$WORK"
|
||||
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 "$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; exit 1; }
|
||||
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
|
||||
exit 1
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
# --8<-- Ende Bootstrap ------------------------------------------------------
|
||||
|
||||
release_holen
|
||||
|
||||
# --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es
|
||||
# bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht.
|
||||
OHNE_RUECKFRAGE=0
|
||||
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
|
||||
[[ "${EXTRA[i]}" == "--yes" || "${EXTRA[i]}" == "-y" ]] && OHNE_RUECKFRAGE=1
|
||||
done
|
||||
|
||||
if [[ $OHNE_RUECKFRAGE -eq 1 ]]; then
|
||||
echo "==> Version $TAG wird ohne Rueckfrage installiert."
|
||||
elif [[ -t 0 ]]; then
|
||||
read -r -p "Version $TAG fuer $APP${EXTRA[0]+ (${EXTRA[*]})} installieren? [j/N] " answer
|
||||
[[ "$answer" =~ ^[jJyY]$ ]] || exit 0
|
||||
else
|
||||
# Ohne Terminal las "read" hier EOF, die Antwort blieb leer, und das Skript
|
||||
# endete mit 0 -- wer es aus einem anderen Skript aufrief, hielt das Update
|
||||
# fuer erledigt. Lieber laut abbrechen.
|
||||
echo "Kein Terminal fuer die Rueckfrage, und --yes fehlt -- es wurde nichts" >&2
|
||||
echo "installiert. Aufruf mit --yes wiederholen." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
exec sudo bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
# Kein exec: sonst laeuft die Aufraeumfalle nie, und jedes Update liesse ein
|
||||
# entpacktes Release unter /tmp liegen.
|
||||
bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
+417
-18
@@ -2,10 +2,17 @@
|
||||
# =============================================================================
|
||||
# Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme.
|
||||
#
|
||||
# Aufruf (aus dem entpackten Release-Verzeichnis):
|
||||
# Eine einzelne Datei genuegt -- sie holt sich das Release selbst:
|
||||
#
|
||||
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
|
||||
# sudo bash upgrade.sh --app tesm
|
||||
#
|
||||
# Auf einer 1.2.8 liegt nichts von diesem Projekt; alles andere waere die
|
||||
# falsche Voraussetzung. Liegt das Skript in einem entpackten Release, wird
|
||||
# dessen install.sh benutzt statt eines Downloads:
|
||||
#
|
||||
# sudo bash deploy/upgrade.sh --app tesm
|
||||
# sudo bash deploy/upgrade.sh --app tesm-license
|
||||
# sudo bash deploy/upgrade.sh --app tesm --instance opus --source /srv/tesm
|
||||
# sudo bash deploy/upgrade.sh --app tesm --instance test --source /srv/tesm
|
||||
#
|
||||
# Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
||||
#
|
||||
@@ -44,6 +51,25 @@ DRY_RUN=0
|
||||
WITH_DATA=1
|
||||
STOP_OLD=1
|
||||
ADOPT_KEY=1
|
||||
TAG="latest"
|
||||
ROLLBACK=""
|
||||
|
||||
#: Steht die Sicherung schon, und ist der Lauf durch? Daran entscheidet
|
||||
#: die EXIT-Falle, ob sie zurueckrollt.
|
||||
SICHERUNG_STEHT=0
|
||||
UPGRADE_FERTIG=0
|
||||
#: Namenszusatz, unter dem ``rename.sh`` eine verdraengte Installation der
|
||||
#: Fassung 1.x ablegt. Beide Skripte muessen denselben Wert benutzen; ein
|
||||
#: Hygienetest vergleicht sie.
|
||||
#:
|
||||
#: Auf einer echten 1.2.8 gibt es das **nicht** -- dort liegt die Installation
|
||||
#: unter ihrem Hauptnamen. Der Zusatz entsteht erst, wenn ``rename.sh`` den
|
||||
#: Hauptnamen fuer die neue Fassung braucht. Deshalb ist er der zweite Ort, an
|
||||
#: dem gesucht wird, nicht der erste.
|
||||
ARCHIV_SUFFIX="sonnet5"
|
||||
|
||||
#: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben?
|
||||
info_quelle=""
|
||||
EXTRA=()
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -73,6 +99,12 @@ Verwendung: upgrade.sh --app <tesm|tesm-license> [Optionen]
|
||||
erzeugen, statt den alten zu uebernehmen. Dann muessen
|
||||
alle Kunden den Ausstellerwechsel bestaetigen -- die
|
||||
Vorgabe ist deshalb die Uebernahme.
|
||||
--tag <vX.Y.Z> Bestimmte Version installieren (Vorgabe: latest).
|
||||
Wird nur beachtet, wenn das Release geladen wird.
|
||||
--rollback [pfad] Einen fehlgeschlagenen Lauf zurueckdrehen: die alte
|
||||
Installation aus der Sicherung wiederherstellen und
|
||||
ihren Dienst wieder einschalten. Ohne Pfad wird die
|
||||
neueste Sicherung dieses Namens genommen.
|
||||
--dry-run Nur zeigen, was geschehen wuerde
|
||||
--yes Keine Rueckfrage
|
||||
-h, --help Diese Hilfe
|
||||
@@ -85,7 +117,7 @@ Beispiele:
|
||||
sudo bash deploy/upgrade.sh --app tesm
|
||||
|
||||
# Daneben, zum Vergleich, alte Installation bleibt unangetastet:
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -99,6 +131,12 @@ while [[ $# -gt 0 ]]; do
|
||||
--keep-old-running) STOP_OLD=0; shift ;;
|
||||
--new-key) ADOPT_KEY=0; shift ;;
|
||||
--adopt-key) ADOPT_KEY=1; shift ;; # Vorgabe, hier nur zur Deutlichkeit
|
||||
--tag) TAG="${2:?}"; shift 2 ;;
|
||||
--rollback)
|
||||
# Der Pfad ist optional: allein genommen heisst es "die neueste".
|
||||
if [[ -n "${2:-}" && "${2:-}" != --* ]]; then ROLLBACK="$2"; shift 2
|
||||
else ROLLBACK="auto"; shift; fi
|
||||
;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--yes|-y) ASSUME_YES=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
@@ -108,37 +146,364 @@ done
|
||||
|
||||
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
|
||||
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
|
||||
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
|
||||
|
||||
[[ -n "$SOURCE" ]] || SOURCE="/srv/$APP"
|
||||
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)"
|
||||
[[ -f "$SOURCE/sqlite.db" ]] || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
|
||||
|
||||
NAME="$APP"
|
||||
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
||||
ADMIN="$NAME-admin"
|
||||
NEW_ROOT="/srv/$NAME"
|
||||
IN_PLACE=0
|
||||
[[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1
|
||||
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP="/srv/$NAME-upgrade-$STAMP"
|
||||
ALT_KOPIE="$BACKUP/alt"
|
||||
SYSTEM_KOPIE="$BACKUP/system"
|
||||
EXPORT_ALT="$BACKUP/export-alt.json"
|
||||
IMPORT_NEU="$BACKUP/import-neu.json"
|
||||
|
||||
# Alte Einheiten der Vorgaengerfassung. Der Timer gehoert dazu: nur den Dienst
|
||||
# zu stoppen liesse ihn beim naechsten Ablauf wieder anlaufen.
|
||||
# -- Rueckweg ---------------------------------------------------------------- #
|
||||
# Alles, was install.sh unter demselben Namen ueberschreiben wuerde. Bei einem
|
||||
# Upgrade in dasselbe Verzeichnis sind das die Dateien der alten Fassung.
|
||||
SYSTEMDATEIEN=(
|
||||
"/etc/systemd/system/$NAME.service"
|
||||
"/etc/systemd/system/$NAME-monitor.service"
|
||||
"/etc/nginx/sites-available/$NAME"
|
||||
"/etc/logrotate.d/$NAME"
|
||||
"/etc/update-motd.d/50-$NAME"
|
||||
"/etc/tesm/$NAME.env"
|
||||
)
|
||||
|
||||
zurueckrollen() {
|
||||
local sicherung="$1" grund="${2:-}"
|
||||
local alt="$sicherung/alt" system="$sicherung/system"
|
||||
[[ -d "$alt" ]] || fail "In $sicherung fehlt das Verzeichnis 'alt' -- das ist keine Sicherung dieses Skripts."
|
||||
|
||||
# Quelle und Art des Umzugs stehen im Manifest. Aus dem laufenden Aufruf
|
||||
# abzuleiten waere falsch: ein Rueckweg kann Tage spaeter und mit anderen
|
||||
# Schaltern aufgerufen werden -- und beim automatischen Rueckweg steht
|
||||
# IN_PLACE zwar fest, beim spaeteren --rollback aber nicht.
|
||||
local ziel="${SOURCE:-}" gleiches_verzeichnis="${IN_PLACE:-0}" vermerkt
|
||||
if [[ -f "$system/manifest" ]]; then
|
||||
vermerkt="$(awk '$1=="quelle" {print $2}' "$system/manifest" | head -1)"
|
||||
[[ -n "$vermerkt" ]] && ziel="$vermerkt"
|
||||
vermerkt="$(awk '$1=="in-place" {print $2}' "$system/manifest" | head -1)"
|
||||
[[ -n "$vermerkt" ]] && gleiches_verzeichnis="$vermerkt"
|
||||
fi
|
||||
[[ -n "$ziel" ]] || fail "Im Manifest fehlt die Quelle -- bitte --source angeben."
|
||||
|
||||
[[ -n "$grund" ]] && warn "$grund"
|
||||
step "Rueckweg: alten Stand wiederherstellen"
|
||||
|
||||
# Der neue Dienst zuerst -- er haelt sonst Dateien offen, die gleich ersetzt
|
||||
# werden.
|
||||
for unit in "$NAME.service" "$NAME-monitor.service"; do
|
||||
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
|
||||
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||
info "$unit gestoppt"
|
||||
fi
|
||||
done
|
||||
|
||||
# Verzeichnis: nur bei gleichem Ziel -- bei einer eigenen Instanz war die
|
||||
# alte Installation nie angefasst.
|
||||
if [[ "$gleiches_verzeichnis" == "1" ]]; then
|
||||
rm -rf "$ziel"
|
||||
mkdir -p "$ziel"
|
||||
tar -C "$alt" -cf - . | tar -C "$ziel" -xf -
|
||||
info "$ziel aus der Sicherung wiederhergestellt"
|
||||
[[ -d "$ziel/venv" ]] || warn "Im Backup war kein venv -- der alte Dienst braucht dann noch 'python3 -m venv venv' und seine requirements.txt."
|
||||
else
|
||||
info "$ziel war nicht betroffen (eigene Instanz) -- bleibt, wie es ist"
|
||||
if [[ -d "$NEW_ROOT" && "$NEW_ROOT" != "$ziel" ]]; then
|
||||
rm -rf "$NEW_ROOT"
|
||||
info "$NEW_ROOT entfernt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Systemdateien zurueck, alles andere entfernen, was der Lauf angelegt hat.
|
||||
local site_wieder=0
|
||||
if [[ -f "$system/manifest" ]]; then
|
||||
while read -r zeile; do
|
||||
case "$zeile" in
|
||||
unit-enabled\ *|site-enabled\ *) continue ;;
|
||||
esac
|
||||
local name; name="$(basename "$zeile")"
|
||||
if [[ -e "$system/$name" ]]; then
|
||||
cp -a "$system/$name" "$zeile"
|
||||
info "wiederhergestellt: $zeile"
|
||||
[[ "$zeile" == /etc/nginx/* ]] && site_wieder=1
|
||||
fi
|
||||
done < <(grep -v '^\(unit\|site\)-enabled ' "$system/manifest" || true)
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
if [[ $site_wieder -eq 1 ]] || [[ -e "/etc/nginx/sites-enabled/$NAME" ]]; then
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 \
|
||||
&& info "nginx neu geladen" || warn "nginx meldet einen Fehler -- bitte 'nginx -t' ansehen"
|
||||
fi
|
||||
|
||||
# Und die Dienste wieder einschalten, die vorher liefen.
|
||||
if [[ -f "$system/manifest" ]]; then
|
||||
while read -r _ unit; do
|
||||
systemctl enable --now "$unit" >/dev/null 2>&1 \
|
||||
&& info "$unit wieder eingeschaltet" || warn "$unit liess sich nicht starten"
|
||||
done < <(grep '^unit-enabled ' "$system/manifest" || true)
|
||||
fi
|
||||
|
||||
step "Zurueckgerollt"
|
||||
info "Sicherung bleibt liegen: $sicherung"
|
||||
# Nicht ueber ALTE_UNITS: die Liste entsteht erst nach der Quellensuche, und
|
||||
# bei "--rollback" laeuft diese Funktion vorher. Das Manifest weiss es.
|
||||
local hinweis
|
||||
hinweis="$(awk '$1=="unit-enabled" {print $2; exit}' "$system/manifest" 2>/dev/null || true)"
|
||||
info "Zustand pruefen: systemctl status ${hinweis:-$APP.service}"
|
||||
}
|
||||
|
||||
# Wird von der EXIT-Falle des Bootstraps aufgerufen -- mit dem Ausstiegscode.
|
||||
# Damit gilt der Rueckweg fuer **jeden** Fehlschlag und nicht nur fuer den, an
|
||||
# den ich gedacht habe: Export, Uebersetzen, Einspielen, nginx, ein Ctrl-C.
|
||||
beim_beenden() {
|
||||
local code="${1:-0}"
|
||||
[[ "$code" == "0" ]] && return 0
|
||||
[[ $UPGRADE_FERTIG -eq 1 ]] && return 0
|
||||
if [[ $SICHERUNG_STEHT -eq 0 ]]; then
|
||||
warn "Abgebrochen (Code $code), bevor eine Sicherung stand -- nichts zurueckzurollen."
|
||||
return 0
|
||||
fi
|
||||
zurueckrollen "$BACKUP" "Abgebrochen (Code $code)."
|
||||
}
|
||||
|
||||
neueste_sicherung() {
|
||||
local kandidat
|
||||
kandidat="$(ls -1dt /srv/"$NAME"-upgrade-*/ 2>/dev/null | head -1)"
|
||||
[[ -n "$kandidat" ]] || return 1
|
||||
printf '%s' "${kandidat%/}"
|
||||
}
|
||||
|
||||
if [[ -n "$ROLLBACK" ]]; then
|
||||
if [[ "$ROLLBACK" == "auto" ]]; then
|
||||
ROLLBACK="$(neueste_sicherung)" || fail "Keine Sicherung unter /srv/$NAME-upgrade-* gefunden. Pfad angeben."
|
||||
info "Neueste Sicherung: $ROLLBACK"
|
||||
fi
|
||||
[[ -d "$ROLLBACK" ]] || fail "$ROLLBACK gibt es nicht"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
step "--dry-run: es wuerde $ROLLBACK zurueckgerollt."
|
||||
exit 0
|
||||
fi
|
||||
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||
read -r -p "Stand aus $ROLLBACK wiederherstellen? [j/N] " answer
|
||||
[[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; }
|
||||
fi
|
||||
zurueckrollen "$ROLLBACK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ab hier wird wirklich installiert -- ein Rueckweg hat oben schon
|
||||
# beendet. Erst jetzt braucht es ein Release und eine Quelle.
|
||||
|
||||
# --8<-- Bootstrap: Release holen --------------------------------------------
|
||||
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
|
||||
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
|
||||
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
|
||||
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
|
||||
#
|
||||
# Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG
|
||||
# (aufgeloest) und $SRC (entpacktes Release). Raeumt sein Arbeitsverzeichnis
|
||||
# beim Beenden auf.
|
||||
#
|
||||
# $REPO gibt es, weil Installationsname und Repositorium auseinanderfallen
|
||||
# koennen: die Master-Ausgabe des Lizenzservers liegt in
|
||||
# "tesm-license-master", die Installation heisst weiter "tesm-license".
|
||||
#
|
||||
# Zwei Fallen des Vorgaengers sind hier vermieden:
|
||||
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
||||
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
||||
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
|
||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
|
||||
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||
|
||||
# Ein privates Repositorium -- tesm-license-master -- gibt sein Release nur mit
|
||||
# Zugriffstoken heraus. Reihenfolge: Umgebungsvariable, dann eine Datei, die
|
||||
# der Betreiber selbst anlegt. Der Installer legt sie nie an: ob ein Token auf
|
||||
# dem Server liegt, ist keine Entscheidung eines Skripts.
|
||||
TOKEN="${TESM_RELEASE_TOKEN:-}"
|
||||
if [[ -z "$TOKEN" && -r /etc/tesm-release.token ]]; then
|
||||
TOKEN="$(tr -d '[:space:]' < /etc/tesm-release.token)"
|
||||
fi
|
||||
|
||||
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
|
||||
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
|
||||
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
|
||||
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
|
||||
# machte weiter, als waere alles in Ordnung.
|
||||
hat_crlf() {
|
||||
local cr
|
||||
cr="$(printf '\r')"
|
||||
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
|
||||
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
|
||||
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
|
||||
LC_ALL=C grep -qU "$cr" "$1"
|
||||
}
|
||||
|
||||
BOOTSTRAP_WORK=""
|
||||
bootstrap_aufraeumen() {
|
||||
# Den Ausstiegscode zuerst festhalten: alles danach ueberschreibt ihn.
|
||||
local code=$?
|
||||
[[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"
|
||||
# Wer einen eigenen Abschluss braucht -- etwa einen Rueckweg nach einem
|
||||
# Fehlschlag --, definiert "beim_beenden". Ein zweiter "trap ... EXIT" waere
|
||||
# der falsche Weg: er wuerde diesen hier ersetzen, und das entpackte Release
|
||||
# bliebe unter /tmp liegen.
|
||||
declare -F beim_beenden >/dev/null 2>&1 && beim_beenden "$code"
|
||||
return "$code"
|
||||
}
|
||||
trap bootstrap_aufraeumen EXIT
|
||||
|
||||
release_holen() {
|
||||
local repo="${TESM_RELEASE_REPO:-${REPO:-$APP}}" asset
|
||||
# Leeres Array und "set -u" vertragen sich nur mit dieser Schreibweise.
|
||||
local -a kopf=()
|
||||
[[ -n "$TOKEN" ]] && kopf=(-H "Authorization: token $TOKEN")
|
||||
for werkzeug in curl tar python3 find; do
|
||||
command -v "$werkzeug" >/dev/null 2>&1 || {
|
||||
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
|
||||
|
||||
if [[ "$TAG" == "latest" ]]; then
|
||||
echo "==> Ermittle aktuellen Release-Tag ..."
|
||||
TAG="$(curl -fsS ${kopf[0]+"${kopf[@]}"} "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
|
||||
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
|
||||
[[ -z "$TOKEN" ]] &&
|
||||
echo "Ist das Repositorium privat? Dann TESM_RELEASE_TOKEN setzen." >&2
|
||||
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
|
||||
return 1
|
||||
}
|
||||
echo " $TAG"
|
||||
fi
|
||||
|
||||
asset="$repo-$TAG.tar.gz"
|
||||
echo "==> Lade $asset ..."
|
||||
curl -fsSL ${kopf[0]+"${kopf[@]}"} -o "$BOOTSTRAP_WORK/$asset" \
|
||||
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
|
||||
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "==> Entpacke ..."
|
||||
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
|
||||
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
|
||||
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
|
||||
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
|
||||
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
|
||||
-printf '%h/..\n' | head -1)"
|
||||
[[ -n "$SRC" ]] || {
|
||||
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
|
||||
return 1
|
||||
}
|
||||
SRC="$(cd "$SRC" && pwd)"
|
||||
|
||||
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
|
||||
if hat_crlf "$SRC/deploy/install.sh"; then
|
||||
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
|
||||
echo "'git -c core.autocrlf=false archive'." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
# --8<-- Ende Bootstrap ------------------------------------------------------
|
||||
|
||||
# Woher kommt install.sh? Liegt dieses Skript in einem entpackten Release,
|
||||
# nimm dessen Verzeichnis -- wer ein Paket ausgepackt hat, will nicht, dass
|
||||
# ungefragt ein anderes geladen wird. Sonst hol das Release.
|
||||
if [[ -f "$SCRIPT_DIR/install.sh" ]]; then
|
||||
SRC="$REPO_DIR"
|
||||
RELEASE_HERKUNFT="$SRC (entpacktes Release neben diesem Skript)"
|
||||
else
|
||||
release_holen || fail "Das Release liess sich nicht holen."
|
||||
RELEASE_HERKUNFT="$TAG von $BASE/$OWNER"
|
||||
fi
|
||||
|
||||
# -- Wo liegt die alte Fassung? ---------------------------------------------- #
|
||||
# Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block:
|
||||
# ``rename.sh`` schiebt eine Installation der Fassung 1.x nach
|
||||
# ``/srv/<app>-sonnet5``, sobald der Hauptname fuer die neue gebraucht wird.
|
||||
# Danach liegt unter ``/srv/<app>`` die **laufende** Installation, und die
|
||||
# Vorgabe zeigte ins Leere. Erkannt wird an ``sqlite.db`` im Wurzelverzeichnis
|
||||
# -- die neue Fassung legt ihre Datenbank unter ``data/app.db`` ab.
|
||||
ist_alte_fassung() { [[ -f "$1/sqlite.db" ]]; }
|
||||
|
||||
if [[ -z "$SOURCE" ]]; then
|
||||
for kandidat in "/srv/$APP" "/srv/$APP-$ARCHIV_SUFFIX"; do
|
||||
if ist_alte_fassung "$kandidat"; then SOURCE="$kandidat"; break; fi
|
||||
done
|
||||
if [[ -z "$SOURCE" ]]; then
|
||||
warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:"
|
||||
warn " /srv/$APP (dort liegt sie normalerweise)"
|
||||
warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie, falls"
|
||||
warn " der Hauptname schon vergeben war)"
|
||||
[[ -f "/srv/$APP/data/app.db" ]] && warn "Unter /srv/$APP liegt die laufende Installation dieser Fassung."
|
||||
fail "Bitte --source setzen."
|
||||
fi
|
||||
info_quelle="gefunden: $SOURCE"
|
||||
fi
|
||||
[[ -d "$SOURCE" ]] || fail "Die alte Installation $SOURCE gibt es nicht (--source setzen)"
|
||||
ist_alte_fassung "$SOURCE" || fail "In $SOURCE liegt keine sqlite.db -- das ist keine Installation der Vorgaengerfassung"
|
||||
|
||||
IN_PLACE=0
|
||||
[[ "$(cd "$SOURCE" && pwd)" == "$NEW_ROOT" ]] && IN_PLACE=1
|
||||
|
||||
# Die Units der alten Fassung erkennt man daran, dass sie in genau dieses
|
||||
# Verzeichnis zeigen -- nicht am Namen der Anwendung. Nach einem rename.sh
|
||||
# heisst die *laufende* Installation "tesm", und eine Namensliste haette sie
|
||||
# gestoppt und deaktiviert, sobald jemand die archivierte 1.x in eine eigene
|
||||
# Instanz hochzieht.
|
||||
#
|
||||
# Timer und Hilfsdienste tragen keinen Pfad (der Restart-Timer ruft nur
|
||||
# "systemctl restart ..."). Sie kommen ueber den Namen der gefundenen Unit mit.
|
||||
alte_units_finden() {
|
||||
local quelle="$1" gefunden=() datei name runde
|
||||
shopt -s nullglob
|
||||
for datei in /etc/systemd/system/*.service; do
|
||||
grep -qs -- "$quelle/" "$datei" && gefunden+=("$(basename "$datei")")
|
||||
done
|
||||
# Zwei Runden: erst Timer und Helfer, die eine gefundene Unit nennen, dann
|
||||
# noch einmal fuer deren eigene Timer.
|
||||
for runde in 1 2; do
|
||||
for datei in /etc/systemd/system/*.service /etc/systemd/system/*.timer; do
|
||||
name="$(basename "$datei")"
|
||||
[[ " ${gefunden[*]-} " == *" $name "* ]] && continue
|
||||
for treffer in ${gefunden[*]-}; do
|
||||
if grep -qs -- "$treffer" "$datei"; then gefunden+=("$name"); break; fi
|
||||
done
|
||||
done
|
||||
done
|
||||
shopt -u nullglob
|
||||
printf '%s\n' ${gefunden[*]-}
|
||||
}
|
||||
|
||||
mapfile -t ALTE_UNITS < <(alte_units_finden "$SOURCE")
|
||||
if [[ ${#ALTE_UNITS[@]} -eq 0 ]]; then
|
||||
if [[ "$SOURCE" == "/srv/$APP" ]]; then
|
||||
# Kein Fund, aber die Quelle ist der Hauptpfad: die historischen Namen der
|
||||
# Fassung 1.x als Rueckfall.
|
||||
if [[ "$APP" == "tesm" ]]; then
|
||||
ALTE_UNITS=(tesm.service tesm-check.timer tesm-check.service
|
||||
tesm-check-restart.timer tesm-check-restart.service)
|
||||
else
|
||||
ALTE_UNITS=(tesm-license.service)
|
||||
fi
|
||||
else
|
||||
ALTE_UNITS=()
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
step "Vorhaben"
|
||||
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||
info "Alt : $SOURCE"
|
||||
info "Neue Fassung : $RELEASE_HERKUNFT"
|
||||
info "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}"
|
||||
info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')"
|
||||
info "Sicherung : $ALT_KOPIE"
|
||||
info "Datenuebernahme: $([[ $WITH_DATA -eq 1 ]] && echo ja || echo nein)"
|
||||
@@ -160,15 +525,45 @@ fi
|
||||
|
||||
# -- 1. Sichern -------------------------------------------------------------- #
|
||||
step "Sichern"
|
||||
mkdir -p "$ALT_KOPIE"
|
||||
mkdir -p "$ALT_KOPIE" "$SYSTEM_KOPIE"
|
||||
chmod 700 "$BACKUP"
|
||||
# -a: Rechte und Zeiten bleiben, damit die Kopie auch als Rueckweg taugt.
|
||||
# Das venv der alten Installation bleibt aussen vor -- es ist nachbaubar,
|
||||
# gross, und seine Shebangs zeigen ohnehin auf den alten Pfad.
|
||||
|
||||
# Das venv kommt nur mit, wenn in dasselbe Verzeichnis installiert wird -- dort
|
||||
# ersetzt install.sh es, und ohne das alte startet der alte Dienst nach einem
|
||||
# Rueckweg nicht mehr. Bei einer eigenen Instanz bleibt die alte Installation
|
||||
# unangetastet, dann waere es nur Ballast (und seine Shebangs zeigen auf einen
|
||||
# anderen Pfad).
|
||||
if [[ $IN_PLACE -eq 1 ]]; then
|
||||
tar -C "$SOURCE" --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf -
|
||||
info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (mit venv, weil dasselbe Verzeichnis)"
|
||||
else
|
||||
tar -C "$SOURCE" --exclude=venv --exclude=__pycache__ -cf - . | tar -C "$ALT_KOPIE" -xf -
|
||||
info "$(du -sh "$ALT_KOPIE" | cut -f1) kopiert (ohne venv)"
|
||||
fi
|
||||
[[ -f "$ALT_KOPIE/sqlite.db" ]] || fail "In der Kopie fehlt die Datenbank -- Abbruch"
|
||||
|
||||
# Und die Systemdateien: bei gleichem Verzeichnis schreibt install.sh Unit,
|
||||
# nginx-Site, logrotate und Banner neu. Ohne Kopie liesse sich der alte Stand
|
||||
# nicht wiederherstellen -- das Verzeichnis allein genuegt nicht.
|
||||
: > "$SYSTEM_KOPIE/manifest"
|
||||
# Woher kam der Stand, und ging es in dasselbe Verzeichnis? Ein spaeterer
|
||||
# Rueckweg darf das nicht raten muessen -- er laeuft womoeglich Tage danach
|
||||
# und mit anderen Schaltern.
|
||||
printf 'quelle %s\n' "$SOURCE" >> "$SYSTEM_KOPIE/manifest"
|
||||
printf 'in-place %s\n' "$IN_PLACE" >> "$SYSTEM_KOPIE/manifest"
|
||||
for pfad in "${SYSTEMDATEIEN[@]}"; do
|
||||
[[ -e "$pfad" ]] || continue
|
||||
cp -a "$pfad" "$SYSTEM_KOPIE/$(basename "$pfad")"
|
||||
printf '%s\n' "$pfad" >> "$SYSTEM_KOPIE/manifest"
|
||||
done
|
||||
# Welche Units liefen, und war die Site eingeschaltet?
|
||||
for unit in "${ALTE_UNITS[@]}"; do
|
||||
systemctl is-enabled "$unit" >/dev/null 2>&1 && printf 'unit-enabled %s\n' "$unit" >> "$SYSTEM_KOPIE/manifest"
|
||||
done
|
||||
[[ -L "/etc/nginx/sites-enabled/$NAME" ]] && printf 'site-enabled %s\n' "$NAME" >> "$SYSTEM_KOPIE/manifest"
|
||||
info "$(grep -c . "$SYSTEM_KOPIE/manifest") Systemdatei(en) und Zustaende vermerkt"
|
||||
SICHERUNG_STEHT=1
|
||||
|
||||
# -- 2. Altes System abschalten ---------------------------------------------- #
|
||||
if [[ $STOP_OLD -eq 1 ]]; then
|
||||
step "Altes System abschalten"
|
||||
@@ -186,9 +581,12 @@ fi
|
||||
step "Neu installieren"
|
||||
INSTALL_ARGS=(--app "$APP" --yes)
|
||||
[[ -n "$INSTANCE" ]] && INSTALL_ARGS+=(--instance "$INSTANCE")
|
||||
bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}" ${EXTRA[0]+"${EXTRA[@]}"}
|
||||
# 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
|
||||
@@ -240,6 +638,7 @@ else
|
||||
fi
|
||||
|
||||
# -- Abschluss --------------------------------------------------------------- #
|
||||
UPGRADE_FERTIG=1
|
||||
step "Fertig"
|
||||
info "Sicherung der alten Installation : $ALT_KOPIE"
|
||||
if [[ "$APP" == "tesm" ]]; then
|
||||
|
||||
+56
-9
@@ -105,19 +105,26 @@ Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
|
||||
|
||||
### Ohne Quellbaum: `update.sh`
|
||||
|
||||
Liegt kein entpacktes Release vor, holt `update.sh` es selbst -- es lädt das
|
||||
Release-Asset, entpackt es und ruft dessen `install.sh` auf:
|
||||
`update.sh` genügt als **einzelne Datei**. Auf einem System, auf dem noch nichts
|
||||
von diesem Projekt liegt:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/tesm/deploy/update.sh tesm
|
||||
sudo bash /opt/tesm-license/deploy/update.sh tesm-license
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||
sudo bash update.sh tesm
|
||||
```
|
||||
|
||||
Auf einer bestehenden Installation liegt sie schon bereit:
|
||||
|
||||
```bash
|
||||
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
|
||||
```
|
||||
|
||||
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||
eine **benannte Instanz** zu aktualisieren:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/tesm-opus/deploy/update.sh tesm latest --instance opus
|
||||
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
```
|
||||
|
||||
Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit
|
||||
@@ -147,11 +154,51 @@ Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte
|
||||
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
|
||||
Exportformat.
|
||||
|
||||
Auch `upgrade.sh` genügt als **einzelne Datei** -- und das ist hier der
|
||||
Regelfall: auf einer 1.2.8 liegt nichts von dieser Fassung.
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh
|
||||
sudo bash upgrade.sh --app tesm
|
||||
```
|
||||
|
||||
Sie lädt das aktuelle Release, entpackt es und benutzt dessen `install.sh`.
|
||||
Liegt das Skript in einem entpackten Release, wird dieses benutzt statt eines
|
||||
Downloads -- wer ein Paket ausgepackt hat, will nicht, dass ungefragt ein
|
||||
anderes geladen wird:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/upgrade.sh --app tesm
|
||||
sudo bash deploy/upgrade.sh --app tesm-license
|
||||
```
|
||||
|
||||
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
|
||||
`/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
|
||||
gebraucht wird -- danach liegt unter `/srv/<app>` die **laufende** Installation.
|
||||
Erkannt wird eine 1.x an der `sqlite.db` im Wurzelverzeichnis; die neue Fassung
|
||||
legt ihre Datenbank unter `data/app.db` ab.
|
||||
|
||||
Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
||||
|
||||
| | |
|
||||
@@ -177,7 +224,7 @@ Export des Vorgängers und taugt deshalb auch als Rückweg.
|
||||
und die alte bleibt vollständig liegen:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running --port 5100 --http-port 8080
|
||||
sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running --port 5100 --http-port 8080
|
||||
```
|
||||
|
||||
**Was nicht mitkommt** -- und warum:
|
||||
@@ -273,7 +320,7 @@ die niemand mehr füllte.
|
||||
Von Hand prüfen:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-opus
|
||||
sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-test
|
||||
```
|
||||
|
||||
Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein
|
||||
@@ -288,8 +335,8 @@ entfernt -- nur dieses, nichts anderes in dem Verzeichnis.
|
||||
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
|
||||
|
||||
```bash
|
||||
sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
||||
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus --to tesm-license
|
||||
sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test --to tesm-license
|
||||
```
|
||||
|
||||
Der Name steckt an mehr Stellen als im Verzeichnisnamen, und drei davon sind
|
||||
|
||||
+181
@@ -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[@]}"}
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.0.7"
|
||||
version = "2.1.2"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -134,7 +134,7 @@ def create_base_app(
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.0.7",
|
||||
app_version: str = "2.1.2",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -76,7 +76,7 @@ class CoreConfig:
|
||||
"""Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""
|
||||
|
||||
site_key: str
|
||||
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-opus``.
|
||||
"""Name *dieser Installation* auf dem Host, z. B. ``tesm-test``.
|
||||
|
||||
Weicht vom ``app_key`` ab, sobald neben der Hauptinstallation eine zweite
|
||||
Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
|
||||
|
||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing-server"
|
||||
version = "2.0.7"
|
||||
version = "2.1.2"
|
||||
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["tesm-licensing==2.0.7", "cryptography>=41"]
|
||||
dependencies = ["tesm-licensing==2.1.2", "cryptography>=41"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.0.7"
|
||||
version = "2.1.2"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in new issue
Block a user