Fork von TESM (srv/tesm), auf Kunden-/Lizenzverwaltung reduziert statt
Geraete-/PoE-Management. Wiederverwendet unveraendert: Login/Session/
Benutzer- und Gruppenverwaltung, LDAP/AD, NGINX- und Systemeinstellungen,
Live-Log/Verlauf/Auditlog, Im-/Export-Grundgeruest, das komplette
PERMISSIONS/NAV_ITEMS/inject_nav()-Rechtesystem sowie das in Phase 1
gebaute Lizenzsystem selbst sowohl fuer die MASTER-eigene Bootstrap-Lizenz
als auch fuer das exakt gleiche licensing.py (byte-identisch zu TESM --
Signieren/Verifizieren muss zwischen beiden Seiten kompatibel bleiben).
Entfernt: Clients/Switche/Zugangsdaten/DHCP/Fileshare/Wartung/Papierkorb/
manueller PoE-Neustart/SSH-Terminal inkl. aller zugehoerigen Tabellen,
Routen, Permissions, Nav-Eintraege und Abhaengigkeiten (paramiko/Flask-Sock/
simple-websocket/PyNaCl/pyasn1 aus requirements.txt). Benutzer-/Gruppen-
Loeschung von Soft- auf Hard-Delete umgestellt (kein Papierkorb mehr).
Eigener Pfad-/Env-Var-Namespace (TESM_LICENSE_* statt TESM_*, /var/log/
tesm-license statt /var/log/tesm usw.), damit Master und TESM testweise
sogar auf demselben Host nebeneinander laufen koennen, ohne sich Log-/
Config-Pfade streitig zu machen.
Neu -- der eigentliche Lizenzserver:
- license_customers/licenses-Tabellen, Master-Signaturschluessel
(master_signing_key.json, einmalig erzeugt, NIE automatisch rotiert --
jede Kundenlizenz traegt den zum Ausstellungszeitpunkt aktuellen
master_pubkey fest eingebettet).
- Dashboard ('/') als Kunden-/Lizenzuebersicht (Typ, Module, Ablauf,
Status, letzter Heartbeat), eigene Kunden-Verwaltungsseite.
- Lizenz-Ausstellung (Typ/Module/Laufzeit -> signierte Datei via
licensing.issue_license), Detailseite, Download, Widerruf.
- /api/activate, /api/deactivate, /api/heartbeat (unauthentifiziert per
Design -- die Signatur der Anfrage IST der Berechtigungsnachweis) sowie
eine manuelle Offline-Code-Seite, die dieselben drei Verarbeitungs-
funktionen (_process_activate/_process_deactivate/_process_heartbeat)
nutzt wie die Online-API -- ein Protokoll, zwei Transportwege.
- E-Mail-Versand ausgestellter Lizenzen per Microsoft Graph
(Client-Credentials-Flow, reine Standardbibliothek/urllib, keine neue
Abhaengigkeit) inkl. Einrichtungsanleitung und Verbindungstest.
- create_master_license.py: lokales Bootstrap-/Erneuerungs-Skript fuer
die eigene Lizenz des Masters (kein externer Super-Master noetig).
Verifiziert auf dem Testsystem (192.168.82.51): App laeuft parallel zu der
dort laufenden echten TESM-Instanz (Port 5001 vs. 80/5000, eigene
Log-/DB-Pfade, TESM unangetastet). Kompletter ECHTER End-to-End-Rundlauf
per Playwright durchgespielt -- kein simulierter Gegenpart: Kunde anlegen
-> Custom-Lizenz (dhcp+fileshare) ausstellen -> herunterladen -> auf der
echten TESM-Instanz hochladen -> ECHTE Online-Aktivierung (Master
verzeichnet Fingerprint, TESM zeigt 'Aktiviert') -> ECHTE Online-
Deaktivierung (TESM zeigt 'lizenzlos, Export bleibt moeglich', Master
zeigt Status 'Deaktiviert'). Lokale Syntax-/Templatepruefung (py_compile,
pyflakes, jinja2-Parse aller Templates) sauber.
Bewusst NICHT nach main gemergt/getaggt/ausgerollt -- folgt zusammen mit
Phase 3 (Infrastruktur-Umzug + POETEST-Enterprise-Lizenz), siehe Plan
toasty-twirling-hickey.md (Phase 2 von 3).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Erzeugt/erneuert die Bootstrap-Lizenz DIESES Lizenzservers selbst.
|
|
|
|
Läuft rein lokal (kein Netzwerk, kein externer Super-Master) -- der Master
|
|
prüft seine eigene Lizenz über denselben license_active()-Mechanismus wie
|
|
Kundeninstanzen (siehe app.py), nur ohne Heartbeat-Ziel: dieses Skript
|
|
setzt den Aktivierungsstatus direkt in der Datenbank, da es keine höhere
|
|
Instanz gibt, die eine Aktivierung bestätigen könnte.
|
|
|
|
Nutzt denselben Master-Signaturschlüssel, den der Server auch zum
|
|
Ausstellen von Kundenlizenzen verwendet (wird beim ersten Aufruf
|
|
automatisch erzeugt, falls noch nicht vorhanden) -- WICHTIG: dieser
|
|
Schlüssel wird beim erneuten Aufruf (Verlängerung) wiederverwendet, nie
|
|
neu erzeugt, siehe _load_or_create_master_signing_key() weiter unten und
|
|
die identische Funktion in app.py: ein Schlüsselwechsel würde die
|
|
Offline-Verifikation künftiger Aktivierungs-/Heartbeat-Antworten für ALLE
|
|
bereits an Kunden ausgegebenen Lizenzen brechen.
|
|
|
|
Aufruf:
|
|
python3 create_master_license.py [--days 730] [--name "Firma XY"]
|
|
|
|
Erneuerung (z.B. kurz vor Ablauf): einfach erneut aufrufen -- überschreibt
|
|
die bestehende Bootstrap-Lizenz, der Signaturschlüssel bleibt unverändert.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import licensing
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
MASTER_SIGNING_KEY_PATH = os.environ.get("TESM_LICENSE_MASTER_KEY", os.path.join(BASE_DIR, "master_signing_key.json"))
|
|
LICENSE_PATH = os.environ.get("TESM_LICENSE_PATH", os.path.join(BASE_DIR, "license.json"))
|
|
DB_PATH = os.environ.get("TESM_LICENSE_DB_PATH", os.path.join(BASE_DIR, "sqlite.db"))
|
|
|
|
|
|
def _load_or_create_master_signing_key():
|
|
try:
|
|
with open(MASTER_SIGNING_KEY_PATH, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return data["private"], data["public"], False
|
|
except (OSError, ValueError, KeyError):
|
|
priv, pub = licensing.generate_keypair()
|
|
with open(MASTER_SIGNING_KEY_PATH, "w", encoding="utf-8") as f:
|
|
json.dump({"private": priv, "public": pub}, f)
|
|
try:
|
|
os.chmod(MASTER_SIGNING_KEY_PATH, 0o600)
|
|
except OSError:
|
|
pass
|
|
return priv, pub, True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Erzeugt/erneuert die Bootstrap-Lizenz dieses Lizenzservers.")
|
|
parser.add_argument("--days", type=int, default=730, help="Gültigkeitsdauer in Tagen (Standard: 730 = 2 Jahre)")
|
|
parser.add_argument("--name", default="Lizenzserver (Eigenbetrieb)", help="Anzeigename in der Lizenzdatei")
|
|
args = parser.parse_args()
|
|
|
|
master_priv, master_pub, created = _load_or_create_master_signing_key()
|
|
if created:
|
|
print(f"Neuer Master-Signaturschlüssel erzeugt: {MASTER_SIGNING_KEY_PATH}")
|
|
else:
|
|
print(f"Bestehenden Master-Signaturschlüssel wiederverwendet: {MASTER_SIGNING_KEY_PATH}")
|
|
|
|
license_file, _pub = licensing.issue_license(
|
|
customer={"name": args.name, "contact_email": ""},
|
|
license_type="enterprise",
|
|
modules=[],
|
|
valid_days=args.days,
|
|
master_private_key_b64=master_priv,
|
|
master_public_key_b64=master_pub,
|
|
master_endpoint="",
|
|
vendor={"name": "", "phone": "", "email": "", "address": "", "logo_base64": ""},
|
|
)
|
|
with open(LICENSE_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(license_file, f, indent=2)
|
|
try:
|
|
os.chmod(LICENSE_PATH, 0o600)
|
|
except OSError:
|
|
pass
|
|
print(f"Bootstrap-Lizenz geschrieben: {LICENSE_PATH} (gültig {args.days} Tage, bis {license_file['expires_at']})")
|
|
|
|
if os.path.exists(DB_PATH):
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.execute(
|
|
"INSERT INTO settings (key, value) VALUES ('license_activation_status', 'active') "
|
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value"
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
print("Aktivierungsstatus in der Datenbank auf 'active' gesetzt.")
|
|
else:
|
|
print(
|
|
f"WARNUNG: Datenbank {DB_PATH} nicht gefunden -- bitte zuerst create_db.py ausführen, "
|
|
"dann dieses Skript erneut aufrufen, damit der Aktivierungsstatus gesetzt werden kann."
|
|
)
|
|
|
|
print("Fertig -- Dienst neu starten (oder warten, bis der Prozess die Lizenz beim nächsten Zugriff neu lädt).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|