Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
119293e5e3 | ||
|
|
36d9cb248e | ||
|
|
cb056b4872 | ||
|
|
ed84e8343e | ||
|
|
157a244900 | ||
|
|
fd333f60ff | ||
|
|
b9342d384c |
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
|
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
|
||||||
```
|
```
|
||||||
@@ -135,3 +150,19 @@ Entwicklungsrepository, ebenso die Tests.
|
|||||||
* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen.
|
* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen.
|
||||||
* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet.
|
* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet.
|
||||||
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
|
* **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]
|
[project]
|
||||||
name = "tesm-license"
|
name = "tesm-license"
|
||||||
version = "2.0.8"
|
version = "2.1.2"
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
+181
-20
@@ -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."
|
||||||
|
# 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() {
|
usage() {
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
||||||
@@ -112,13 +171,27 @@ 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"
|
||||||
|
# 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
|
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 +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)"
|
[[ -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 +305,41 @@ 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.
|
||||||
|
# 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 ------------------------------------------------------------------
|
# -- 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 +469,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 +613,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 +628,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 +690,41 @@ 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
|
||||||
|
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 +740,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
@@ -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"
|
||||||
|
|||||||
+3
-3
@@ -3,8 +3,8 @@
|
|||||||
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
|
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
|
||||||
# (oder umgekehrt).
|
# (oder umgekehrt).
|
||||||
#
|
#
|
||||||
# sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
# sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||||
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus \
|
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test \
|
||||||
# --to tesm-license
|
# --to tesm-license
|
||||||
#
|
#
|
||||||
# Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses
|
# 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]
|
Verwendung: rename.sh --app <tesm|tesm-license> --from <alt> --to <neu> [Optionen]
|
||||||
|
|
||||||
--app <name> Welche Anwendung (Pflicht)
|
--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)
|
--to <name> Neuer Installationsname, z.B. tesm (Pflicht)
|
||||||
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
|
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
|
||||||
der Fassung 1.x (Vorgabe: sonnet5)
|
der Fassung 1.x (Vorgabe: sonnet5)
|
||||||
|
|||||||
+7
-1
@@ -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
|
||||||
@@ -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
|
||||||
+1
-1
@@ -47,7 +47,7 @@ check_in_list() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Name einer Installation auf diesem Host: entweder die Hauptinstallation
|
# 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
|
# beiden Formen gibt es nicht -- so kann keine erfundene Zeichenkette einen
|
||||||
# Pfad ausserhalb des vorgesehenen Namensraums erzeugen.
|
# Pfad ausserhalb des vorgesehenen Namensraums erzeugen.
|
||||||
check_app_key() {
|
check_app_key() {
|
||||||
|
|||||||
+171
-61
@@ -1,89 +1,199 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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.
|
# Enthaelt selbst keinerlei Installationslogik.
|
||||||
#
|
#
|
||||||
# Zwei Fallen des Vorgaengers sind hier bewusst vermieden:
|
# Eine einzelne Datei genuegt -- sie holt sich alles Weitere:
|
||||||
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
#
|
||||||
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
# curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||||
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Assets.
|
# sudo bash update.sh tesm
|
||||||
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert -- faengt
|
#
|
||||||
# denselben Fehler ein zweites Mal ab.
|
# 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
|
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:-}"
|
APP="${1:-}"
|
||||||
TAG="${2:-latest}"
|
TAG="${2:-latest}"
|
||||||
|
|
||||||
# Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse
|
# Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse
|
||||||
# sich eine benannte Instanz nicht aktualisieren: install.sh leitet den Namen
|
# 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
|
# zweite Installation namens "tesm" -- mit eigener Datenbank, eigener
|
||||||
# nginx-Site und demselben Port.
|
# nginx-Site und demselben Port.
|
||||||
shift $(( $# > 2 ? 2 : $# ))
|
shift $(( $# > 2 ? 2 : $# ))
|
||||||
EXTRA=("$@")
|
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" ]] || {
|
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || {
|
||||||
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
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
|
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".
|
||||||
WORK="$(mktemp -d /tmp/tesm-update-XXXXXX)"
|
# Erkannt wird sie an der Installation und nicht an einem Schalter -- wer
|
||||||
trap 'rm -rf "$WORK"' EXIT # anders als im Vorgaenger wird aufgeraeumt
|
# aktualisiert, soll nicht erst wissen muessen, welche Ausgabe hier laeuft.
|
||||||
|
# Sonst holte der Aufruf die Ausgabe fuer Sub-Lizenzserver, und der Installer
|
||||||
if [[ "$TAG" == "latest" ]]; then
|
# muesste sie abweisen.
|
||||||
echo "==> Ermittle aktuellen Release-Tag ..."
|
REPO="$APP"
|
||||||
TAG="$(curl -fsS "$BASE/api/v1/repos/$OWNER/$REPO/releases/latest" \
|
if [[ "$APP" == "tesm-license" ]]; then
|
||||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
|
NAME="$APP"
|
||||||
echo " $TAG"
|
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
|
fi
|
||||||
|
|
||||||
ASSET="$REPO-$TAG.tar.gz"
|
# --8<-- Bootstrap: Release holen --------------------------------------------
|
||||||
echo "==> Lade $ASSET ..."
|
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
|
||||||
curl -fsSL -o "$WORK/$ASSET" "$BASE/$OWNER/$REPO/releases/download/$TAG/$ASSET"
|
# 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}"
|
||||||
|
|
||||||
echo "==> Entpacke ..."
|
# Ein privates Repositorium -- tesm-license-master -- gibt sein Release nur mit
|
||||||
tar -xzf "$WORK/$ASSET" -C "$WORK"
|
# Zugriffstoken heraus. Reihenfolge: Umgebungsvariable, dann eine Datei, die
|
||||||
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
|
# der Betreiber selbst anlegt. Der Installer legt sie nie an: ob ein Token auf
|
||||||
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
|
# dem Server liegt, ist keine Entscheidung eines Skripts.
|
||||||
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
|
TOKEN="${TESM_RELEASE_TOKEN:-}"
|
||||||
SRC="$(find "$WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' -printf '%h/..\n' | head -1)"
|
if [[ -z "$TOKEN" && -r /etc/tesm-release.token ]]; then
|
||||||
[[ -n "$SRC" ]] || { echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2; exit 1; }
|
TOKEN="$(tr -d '[:space:]' < /etc/tesm-release.token)"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -r -p "Version $TAG fuer $APP${EXTRA[0]+ (${EXTRA[*]})} installieren? [j/N] " answer
|
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
|
||||||
[[ "$answer" =~ ^[jJyY]$ ]] || exit 0
|
# ("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"
|
||||||
|
}
|
||||||
|
|
||||||
exec sudo bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}
|
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 ------------------------------------------------------
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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[@]}"}
|
||||||
+401
-30
@@ -2,10 +2,17 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Upgrade vom Stand SONNET5 (Version 1.x) auf diese Fassung -- mit Datenuebernahme.
|
# 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
|
||||||
# sudo bash deploy/upgrade.sh --app tesm-license
|
# sudo bash deploy/upgrade.sh --app tesm --instance test --source /srv/tesm
|
||||||
# sudo bash deploy/upgrade.sh --app tesm --instance opus --source /srv/tesm
|
|
||||||
#
|
#
|
||||||
# Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
# Der Ablauf ist die Vorgabe und braucht keine Schalter:
|
||||||
#
|
#
|
||||||
@@ -44,9 +51,21 @@ DRY_RUN=0
|
|||||||
WITH_DATA=1
|
WITH_DATA=1
|
||||||
STOP_OLD=1
|
STOP_OLD=1
|
||||||
ADOPT_KEY=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
|
#: 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.
|
||||||
|
#:
|
||||||
|
#: 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"
|
ARCHIV_SUFFIX="sonnet5"
|
||||||
|
|
||||||
#: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben?
|
#: Nur fuer die Ausgabe: wurde die Quelle selbst gefunden oder angegeben?
|
||||||
@@ -80,6 +99,12 @@ Verwendung: upgrade.sh --app <tesm|tesm-license> [Optionen]
|
|||||||
erzeugen, statt den alten zu uebernehmen. Dann muessen
|
erzeugen, statt den alten zu uebernehmen. Dann muessen
|
||||||
alle Kunden den Ausstellerwechsel bestaetigen -- die
|
alle Kunden den Ausstellerwechsel bestaetigen -- die
|
||||||
Vorgabe ist deshalb die Uebernahme.
|
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
|
--dry-run Nur zeigen, was geschehen wuerde
|
||||||
--yes Keine Rueckfrage
|
--yes Keine Rueckfrage
|
||||||
-h, --help Diese Hilfe
|
-h, --help Diese Hilfe
|
||||||
@@ -92,7 +117,7 @@ Beispiele:
|
|||||||
sudo bash deploy/upgrade.sh --app tesm
|
sudo bash deploy/upgrade.sh --app tesm
|
||||||
|
|
||||||
# Daneben, zum Vergleich, alte Installation bleibt unangetastet:
|
# 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
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +131,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--keep-old-running) STOP_OLD=0; shift ;;
|
--keep-old-running) STOP_OLD=0; shift ;;
|
||||||
--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 ;;
|
||||||
|
--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 ;;
|
||||||
@@ -115,7 +146,284 @@ 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"
|
||||||
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
|
|
||||||
|
NAME="$APP"
|
||||||
|
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
||||||
|
ADMIN="$NAME-admin"
|
||||||
|
NEW_ROOT="/srv/$NAME"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP="/srv/$NAME-upgrade-$STAMP"
|
||||||
|
ALT_KOPIE="$BACKUP/alt"
|
||||||
|
SYSTEM_KOPIE="$BACKUP/system"
|
||||||
|
EXPORT_ALT="$BACKUP/export-alt.json"
|
||||||
|
IMPORT_NEU="$BACKUP/import-neu.json"
|
||||||
|
|
||||||
|
# Alte Einheiten der Vorgaengerfassung. Der Timer gehoert dazu: nur den Dienst
|
||||||
|
# zu stoppen liesse ihn beim naechsten Ablauf wieder anlaufen.
|
||||||
|
# -- Rueckweg ---------------------------------------------------------------- #
|
||||||
|
# Alles, was install.sh unter demselben Namen ueberschreiben wuerde. Bei einem
|
||||||
|
# Upgrade in dasselbe Verzeichnis sind das die Dateien der alten Fassung.
|
||||||
|
SYSTEMDATEIEN=(
|
||||||
|
"/etc/systemd/system/$NAME.service"
|
||||||
|
"/etc/systemd/system/$NAME-monitor.service"
|
||||||
|
"/etc/nginx/sites-available/$NAME"
|
||||||
|
"/etc/logrotate.d/$NAME"
|
||||||
|
"/etc/update-motd.d/50-$NAME"
|
||||||
|
"/etc/tesm/$NAME.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
zurueckrollen() {
|
||||||
|
local sicherung="$1" grund="${2:-}"
|
||||||
|
local alt="$sicherung/alt" system="$sicherung/system"
|
||||||
|
[[ -d "$alt" ]] || fail "In $sicherung fehlt das Verzeichnis 'alt' -- das ist keine Sicherung dieses Skripts."
|
||||||
|
|
||||||
|
# Quelle und Art des Umzugs stehen im Manifest. Aus dem laufenden Aufruf
|
||||||
|
# abzuleiten waere falsch: ein Rueckweg kann Tage spaeter und mit anderen
|
||||||
|
# Schaltern aufgerufen werden -- und beim automatischen Rueckweg steht
|
||||||
|
# IN_PLACE zwar fest, beim spaeteren --rollback aber nicht.
|
||||||
|
local ziel="${SOURCE:-}" gleiches_verzeichnis="${IN_PLACE:-0}" vermerkt
|
||||||
|
if [[ -f "$system/manifest" ]]; then
|
||||||
|
vermerkt="$(awk '$1=="quelle" {print $2}' "$system/manifest" | head -1)"
|
||||||
|
[[ -n "$vermerkt" ]] && ziel="$vermerkt"
|
||||||
|
vermerkt="$(awk '$1=="in-place" {print $2}' "$system/manifest" | head -1)"
|
||||||
|
[[ -n "$vermerkt" ]] && gleiches_verzeichnis="$vermerkt"
|
||||||
|
fi
|
||||||
|
[[ -n "$ziel" ]] || fail "Im Manifest fehlt die Quelle -- bitte --source angeben."
|
||||||
|
|
||||||
|
[[ -n "$grund" ]] && warn "$grund"
|
||||||
|
step "Rueckweg: alten Stand wiederherstellen"
|
||||||
|
|
||||||
|
# Der neue Dienst zuerst -- er haelt sonst Dateien offen, die gleich ersetzt
|
||||||
|
# werden.
|
||||||
|
for unit in "$NAME.service" "$NAME-monitor.service"; do
|
||||||
|
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
|
||||||
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||||
|
info "$unit gestoppt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verzeichnis: nur bei gleichem Ziel -- bei einer eigenen Instanz war die
|
||||||
|
# alte Installation nie angefasst.
|
||||||
|
if [[ "$gleiches_verzeichnis" == "1" ]]; then
|
||||||
|
rm -rf "$ziel"
|
||||||
|
mkdir -p "$ziel"
|
||||||
|
tar -C "$alt" -cf - . | tar -C "$ziel" -xf -
|
||||||
|
info "$ziel aus der Sicherung wiederhergestellt"
|
||||||
|
[[ -d "$ziel/venv" ]] || warn "Im Backup war kein venv -- der alte Dienst braucht dann noch 'python3 -m venv venv' und seine requirements.txt."
|
||||||
|
else
|
||||||
|
info "$ziel war nicht betroffen (eigene Instanz) -- bleibt, wie es ist"
|
||||||
|
if [[ -d "$NEW_ROOT" && "$NEW_ROOT" != "$ziel" ]]; then
|
||||||
|
rm -rf "$NEW_ROOT"
|
||||||
|
info "$NEW_ROOT entfernt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Systemdateien zurueck, alles andere entfernen, was der Lauf angelegt hat.
|
||||||
|
local site_wieder=0
|
||||||
|
if [[ -f "$system/manifest" ]]; then
|
||||||
|
while read -r zeile; do
|
||||||
|
case "$zeile" in
|
||||||
|
unit-enabled\ *|site-enabled\ *) continue ;;
|
||||||
|
esac
|
||||||
|
local name; name="$(basename "$zeile")"
|
||||||
|
if [[ -e "$system/$name" ]]; then
|
||||||
|
cp -a "$system/$name" "$zeile"
|
||||||
|
info "wiederhergestellt: $zeile"
|
||||||
|
[[ "$zeile" == /etc/nginx/* ]] && site_wieder=1
|
||||||
|
fi
|
||||||
|
done < <(grep -v '^\(unit\|site\)-enabled ' "$system/manifest" || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
if [[ $site_wieder -eq 1 ]] || [[ -e "/etc/nginx/sites-enabled/$NAME" ]]; then
|
||||||
|
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 \
|
||||||
|
&& info "nginx neu geladen" || warn "nginx meldet einen Fehler -- bitte 'nginx -t' ansehen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Und die Dienste wieder einschalten, die vorher liefen.
|
||||||
|
if [[ -f "$system/manifest" ]]; then
|
||||||
|
while read -r _ unit; do
|
||||||
|
systemctl enable --now "$unit" >/dev/null 2>&1 \
|
||||||
|
&& info "$unit wieder eingeschaltet" || warn "$unit liess sich nicht starten"
|
||||||
|
done < <(grep '^unit-enabled ' "$system/manifest" || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Zurueckgerollt"
|
||||||
|
info "Sicherung bleibt liegen: $sicherung"
|
||||||
|
# Nicht ueber ALTE_UNITS: die Liste entsteht erst nach der Quellensuche, und
|
||||||
|
# bei "--rollback" laeuft diese Funktion vorher. Das Manifest weiss es.
|
||||||
|
local hinweis
|
||||||
|
hinweis="$(awk '$1=="unit-enabled" {print $2; exit}' "$system/manifest" 2>/dev/null || true)"
|
||||||
|
info "Zustand pruefen: systemctl status ${hinweis:-$APP.service}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wird von der EXIT-Falle des Bootstraps aufgerufen -- mit dem Ausstiegscode.
|
||||||
|
# Damit gilt der Rueckweg fuer **jeden** Fehlschlag und nicht nur fuer den, an
|
||||||
|
# den ich gedacht habe: Export, Uebersetzen, Einspielen, nginx, ein Ctrl-C.
|
||||||
|
beim_beenden() {
|
||||||
|
local code="${1:-0}"
|
||||||
|
[[ "$code" == "0" ]] && return 0
|
||||||
|
[[ $UPGRADE_FERTIG -eq 1 ]] && return 0
|
||||||
|
if [[ $SICHERUNG_STEHT -eq 0 ]]; then
|
||||||
|
warn "Abgebrochen (Code $code), bevor eine Sicherung stand -- nichts zurueckzurollen."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
zurueckrollen "$BACKUP" "Abgebrochen (Code $code)."
|
||||||
|
}
|
||||||
|
|
||||||
|
neueste_sicherung() {
|
||||||
|
local kandidat
|
||||||
|
kandidat="$(ls -1dt /srv/"$NAME"-upgrade-*/ 2>/dev/null | head -1)"
|
||||||
|
[[ -n "$kandidat" ]] || return 1
|
||||||
|
printf '%s' "${kandidat%/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$ROLLBACK" ]]; then
|
||||||
|
if [[ "$ROLLBACK" == "auto" ]]; then
|
||||||
|
ROLLBACK="$(neueste_sicherung)" || fail "Keine Sicherung unter /srv/$NAME-upgrade-* gefunden. Pfad angeben."
|
||||||
|
info "Neueste Sicherung: $ROLLBACK"
|
||||||
|
fi
|
||||||
|
[[ -d "$ROLLBACK" ]] || fail "$ROLLBACK gibt es nicht"
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
step "--dry-run: es wuerde $ROLLBACK zurueckgerollt."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
|
read -r -p "Stand aus $ROLLBACK wiederherstellen? [j/N] " answer
|
||||||
|
[[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; }
|
||||||
|
fi
|
||||||
|
zurueckrollen "$ROLLBACK"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ab hier wird wirklich installiert -- ein Rueckweg hat oben schon
|
||||||
|
# beendet. Erst jetzt braucht es ein Release und eine Quelle.
|
||||||
|
|
||||||
|
# --8<-- Bootstrap: Release holen --------------------------------------------
|
||||||
|
# Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden
|
||||||
|
# Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und
|
||||||
|
# beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen,
|
||||||
|
# ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt.
|
||||||
|
#
|
||||||
|
# Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG
|
||||||
|
# (aufgeloest) und $SRC (entpacktes Release). Raeumt sein Arbeitsverzeichnis
|
||||||
|
# beim Beenden auf.
|
||||||
|
#
|
||||||
|
# $REPO gibt es, weil Installationsname und Repositorium auseinanderfallen
|
||||||
|
# koennen: die Master-Ausgabe des Lizenzservers liegt in
|
||||||
|
# "tesm-license-master", die Installation heisst weiter "tesm-license".
|
||||||
|
#
|
||||||
|
# Zwei Fallen des Vorgaengers sind hier vermieden:
|
||||||
|
# * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die
|
||||||
|
# Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein
|
||||||
|
# automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs.
|
||||||
|
# * Nach dem Entpacken wird geprueft, dass ein install.sh existiert.
|
||||||
|
BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}"
|
||||||
|
OWNER="${TESM_RELEASE_OWNER:-alientim}"
|
||||||
|
|
||||||
|
# Ein privates Repositorium -- tesm-license-master -- gibt sein Release nur mit
|
||||||
|
# Zugriffstoken heraus. Reihenfolge: Umgebungsvariable, dann eine Datei, die
|
||||||
|
# der Betreiber selbst anlegt. Der Installer legt sie nie an: ob ein Token auf
|
||||||
|
# dem Server liegt, ist keine Entscheidung eines Skripts.
|
||||||
|
TOKEN="${TESM_RELEASE_TOKEN:-}"
|
||||||
|
if [[ -z "$TOKEN" && -r /etc/tesm-release.token ]]; then
|
||||||
|
TOKEN="$(tr -d '[:space:]' < /etc/tesm-release.token)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar
|
||||||
|
# ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem
|
||||||
|
# Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt --
|
||||||
|
# endete sie in "command not found", grep bekam nichts zu sehen, und das Skript
|
||||||
|
# machte weiter, als waere alles in Ordnung.
|
||||||
|
hat_crlf() {
|
||||||
|
local cr
|
||||||
|
cr="$(printf '\r')"
|
||||||
|
# -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer
|
||||||
|
# Windows-Buildmaschine aber entscheidend: dort entfernt grep den
|
||||||
|
# Wagenruecklauf beim Lesen und findet deshalb nie einen.
|
||||||
|
LC_ALL=C grep -qU "$cr" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOTSTRAP_WORK=""
|
||||||
|
bootstrap_aufraeumen() {
|
||||||
|
# Den Ausstiegscode zuerst festhalten: alles danach ueberschreibt ihn.
|
||||||
|
local code=$?
|
||||||
|
[[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK"
|
||||||
|
# Wer einen eigenen Abschluss braucht -- etwa einen Rueckweg nach einem
|
||||||
|
# Fehlschlag --, definiert "beim_beenden". Ein zweiter "trap ... EXIT" waere
|
||||||
|
# der falsche Weg: er wuerde diesen hier ersetzen, und das entpackte Release
|
||||||
|
# bliebe unter /tmp liegen.
|
||||||
|
declare -F beim_beenden >/dev/null 2>&1 && beim_beenden "$code"
|
||||||
|
return "$code"
|
||||||
|
}
|
||||||
|
trap bootstrap_aufraeumen EXIT
|
||||||
|
|
||||||
|
release_holen() {
|
||||||
|
local repo="${TESM_RELEASE_REPO:-${REPO:-$APP}}" asset
|
||||||
|
# Leeres Array und "set -u" vertragen sich nur mit dieser Schreibweise.
|
||||||
|
local -a kopf=()
|
||||||
|
[[ -n "$TOKEN" ]] && kopf=(-H "Authorization: token $TOKEN")
|
||||||
|
for werkzeug in curl tar python3 find; do
|
||||||
|
command -v "$werkzeug" >/dev/null 2>&1 || {
|
||||||
|
echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)"
|
||||||
|
|
||||||
|
if [[ "$TAG" == "latest" ]]; then
|
||||||
|
echo "==> Ermittle aktuellen Release-Tag ..."
|
||||||
|
TAG="$(curl -fsS ${kopf[0]+"${kopf[@]}"} "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \
|
||||||
|
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || {
|
||||||
|
echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2
|
||||||
|
[[ -z "$TOKEN" ]] &&
|
||||||
|
echo "Ist das Repositorium privat? Dann TESM_RELEASE_TOKEN setzen." >&2
|
||||||
|
echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
echo " $TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
asset="$repo-$TAG.tar.gz"
|
||||||
|
echo "==> Lade $asset ..."
|
||||||
|
curl -fsSL ${kopf[0]+"${kopf[@]}"} -o "$BOOTSTRAP_WORK/$asset" \
|
||||||
|
"$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || {
|
||||||
|
echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> Entpacke ..."
|
||||||
|
tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK"
|
||||||
|
# Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "<name>/", also
|
||||||
|
# liegt das Skript unter <name>/deploy/install.sh. Mit "-maxdepth 2" fand die
|
||||||
|
# Suche nie etwas -- und die Fehlermeldung schob es auf den Download.
|
||||||
|
SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \
|
||||||
|
-printf '%h/..\n' | head -1)"
|
||||||
|
[[ -n "$SRC" ]] || {
|
||||||
|
echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
SRC="$(cd "$SRC" && pwd)"
|
||||||
|
|
||||||
|
echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..."
|
||||||
|
if hat_crlf "$SRC/deploy/install.sh"; then
|
||||||
|
echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2
|
||||||
|
echo "'git -c core.autocrlf=false archive'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# --8<-- Ende Bootstrap ------------------------------------------------------
|
||||||
|
|
||||||
|
# Woher kommt install.sh? Liegt dieses Skript in einem entpackten Release,
|
||||||
|
# nimm dessen Verzeichnis -- wer ein Paket ausgepackt hat, will nicht, dass
|
||||||
|
# ungefragt ein anderes geladen wird. Sonst hol das Release.
|
||||||
|
if [[ -f "$SCRIPT_DIR/install.sh" ]]; then
|
||||||
|
SRC="$REPO_DIR"
|
||||||
|
RELEASE_HERKUNFT="$SRC (entpacktes Release neben diesem Skript)"
|
||||||
|
else
|
||||||
|
release_holen || fail "Das Release liess sich nicht holen."
|
||||||
|
RELEASE_HERKUNFT="$TAG von $BASE/$OWNER"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- Wo liegt die alte Fassung? ---------------------------------------------- #
|
# -- Wo liegt die alte Fassung? ---------------------------------------------- #
|
||||||
# Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block:
|
# Zwei Orte kommen in Frage, und der zweite ist der Grund fuer diesen Block:
|
||||||
@@ -132,8 +440,9 @@ if [[ -z "$SOURCE" ]]; then
|
|||||||
done
|
done
|
||||||
if [[ -z "$SOURCE" ]]; then
|
if [[ -z "$SOURCE" ]]; then
|
||||||
warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:"
|
warn "Keine Installation der Fassung 1.x gefunden. Gesucht wurde in:"
|
||||||
warn " /srv/$APP"
|
warn " /srv/$APP (dort liegt sie normalerweise)"
|
||||||
warn " /srv/$APP-$ARCHIV_SUFFIX (dorthin verschiebt rename.sh sie)"
|
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."
|
[[ -f "/srv/$APP/data/app.db" ]] && warn "Unter /srv/$APP liegt die laufende Installation dieser Fassung."
|
||||||
fail "Bitte --source setzen."
|
fail "Bitte --source setzen."
|
||||||
fi
|
fi
|
||||||
@@ -142,30 +451,58 @@ 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 "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}"
|
info "Alt : $SOURCE${info_quelle:+ (automatisch gefunden)}"
|
||||||
info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')"
|
info "Neu : $NEW_ROOT$([[ $IN_PLACE -eq 1 ]] && echo ' << dasselbe Verzeichnis')"
|
||||||
info "Sicherung : $ALT_KOPIE"
|
info "Sicherung : $ALT_KOPIE"
|
||||||
@@ -188,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"
|
||||||
@@ -214,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")
|
||||||
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
|
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
|
||||||
@@ -268,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
|
||||||
|
|||||||
+49
-9
@@ -105,19 +105,26 @@ Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
|
|||||||
|
|
||||||
### Ohne Quellbaum: `update.sh`
|
### Ohne Quellbaum: `update.sh`
|
||||||
|
|
||||||
Liegt kein entpacktes Release vor, holt `update.sh` es selbst -- es lädt das
|
`update.sh` genügt als **einzelne Datei**. Auf einem System, auf dem noch nichts
|
||||||
Release-Asset, entpackt es und ruft dessen `install.sh` auf:
|
von diesem Projekt liegt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo bash /opt/tesm/deploy/update.sh tesm
|
curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh
|
||||||
sudo bash /opt/tesm-license/deploy/update.sh tesm-license
|
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,
|
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||||
eine **benannte Instanz** zu aktualisieren:
|
eine **benannte Instanz** zu aktualisieren:
|
||||||
|
|
||||||
```bash
|
```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
|
Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit
|
||||||
@@ -147,11 +154,44 @@ Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte
|
|||||||
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
|
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
|
||||||
Exportformat.
|
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
|
```bash
|
||||||
sudo bash deploy/upgrade.sh --app tesm
|
sudo bash deploy/upgrade.sh --app tesm
|
||||||
sudo bash deploy/upgrade.sh --app tesm-license
|
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
|
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
|
||||||
@@ -184,7 +224,7 @@ Export des Vorgängers und taugt deshalb auch als Rückweg.
|
|||||||
und die alte bleibt vollständig liegen:
|
und die alte bleibt vollständig liegen:
|
||||||
|
|
||||||
```bash
|
```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:
|
**Was nicht mitkommt** -- und warum:
|
||||||
@@ -280,7 +320,7 @@ die niemand mehr füllte.
|
|||||||
Von Hand prüfen:
|
Von Hand prüfen:
|
||||||
|
|
||||||
```bash
|
```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
|
Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein
|
||||||
@@ -295,8 +335,8 @@ entfernt -- nur dieses, nichts anderes in dem Verzeichnis.
|
|||||||
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
|
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm
|
||||||
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus --to tesm-license
|
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
|
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]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.0.8"
|
version = "2.1.2"
|
||||||
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.8",
|
app_version: str = "2.1.2",
|
||||||
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,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class CoreConfig:
|
|||||||
"""Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""
|
"""Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""
|
||||||
|
|
||||||
site_key: str
|
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
|
Weicht vom ``app_key`` ab, sobald neben der Hauptinstallation eine zweite
|
||||||
Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
|
Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing-server"
|
name = "tesm-licensing-server"
|
||||||
version = "2.0.8"
|
version = "2.1.2"
|
||||||
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.8", "cryptography>=41"]
|
dependencies = ["tesm-licensing==2.1.2", "cryptography>=41"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.0.8"
|
version = "2.1.2"
|
||||||
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"]
|
||||||
|
|||||||
Reference in new issue
Block a user