TESM-Lizenzserver 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert im Zweig SONNET5 erhalten. Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern), packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd, privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich jedes allein installieren laesst. Die wichtigsten Unterschiede zum Vorgaenger: * Keine doppelte licensing.py -- ein Paket, das beide Anwendungen installieren, statt zweier Dateien, die byte-identisch bleiben sollen. * Der Webprozess laeuft unprivilegiert; alles, was Root braucht, geht ueber einen einzigen Helfer mit Positivlisten fuer jedes Argument. * CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen. * Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet. * Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann. * Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als "Aktivierung offen" markiert, bis sie zurueckkommt. * Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung, seq gegen das Zurueckrollen auf eine aeltere Lizenz. * Kein Hostname im Maschinen-Fingerabdruck. * Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
"""End-to-End: der komplette Lizenz-Lebenszyklus ueber beide Anwendungen hinweg.
|
||||
|
||||
Laeuft auf dem Zielhost gegen zwei echte, laufende Installationen:
|
||||
|
||||
Lizenzserver: Kunde -> Ticket -> Lizenz ausstellen -> Bundle herunterladen
|
||||
TESM : Bundle einspielen -> aktivieren (online gegen den Lizenzserver)
|
||||
-> Module freigeschaltet -> Heartbeat -> Revision -> Widerruf
|
||||
|
||||
Das ist der Pfad, der im Vorgaengerprojekt nur von Hand pruefbar war -- und
|
||||
genau dort sassen die teuren Fehler. Geprueft wird deshalb nicht nur "es geht",
|
||||
sondern auch: Doppelausstellung wird verhindert, Module sind vor der
|
||||
Aktivierung wirklich gesperrt, eine neue Revision nimmt Module wieder weg, und
|
||||
ein Widerruf kommt beim Client an.
|
||||
|
||||
Aufruf:
|
||||
python3 e2e_license.py http://127.0.0.1:8081 http://127.0.0.1:8080 admin 'Passwort'
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.cookiejar
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
CSRF_RE = re.compile(r'name="csrf_token" value="([^"]+)"')
|
||||
BODY_CSRF_RE = re.compile(r'data-csrf="([^"]+)"')
|
||||
|
||||
PASSED: list[str] = []
|
||||
FAILED: list[str] = []
|
||||
|
||||
|
||||
def check(name: str, condition: bool, detail: str = "") -> None:
|
||||
if condition:
|
||||
PASSED.append(name)
|
||||
print(f" [ok] {name}")
|
||||
else:
|
||||
FAILED.append(f"{name}: {detail}")
|
||||
print(f" [FEHL] {name} -- {detail}")
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, status: int, body: bytes, url: str, headers: dict[str, str]) -> None:
|
||||
self.status = status
|
||||
self.raw = body
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self.raw.decode("utf-8", "replace")
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, base: str, label: str) -> None:
|
||||
self.base = base.rstrip("/")
|
||||
self.label = label
|
||||
jar = http.cookiejar.CookieJar()
|
||||
# Der Test laeuft auf dem Zielhost gegen dessen eigenes, selbst
|
||||
# ausgestelltes Zertifikat. Geprueft wird, dass TLS steht und die
|
||||
# richtige Anwendung antwortet -- nicht die Vertrauenskette.
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar),
|
||||
urllib.request.HTTPSHandler(context=context),
|
||||
)
|
||||
self.opener.addheaders = [("User-Agent", "tesm-e2e-license/2.0")]
|
||||
|
||||
def _open(self, request: urllib.request.Request) -> Response:
|
||||
try:
|
||||
with self.opener.open(request, timeout=60) as response:
|
||||
return Response(
|
||||
response.status, response.read(), response.geturl(), dict(response.headers)
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
return Response(exc.code, exc.read(), exc.url, dict(exc.headers))
|
||||
|
||||
def get(self, path: str) -> Response:
|
||||
return self._open(urllib.request.Request(self.base + path))
|
||||
|
||||
def text(self, path: str) -> str:
|
||||
return self.get(path).text
|
||||
|
||||
def post(self, path: str, data: dict[str, object]) -> Response:
|
||||
pairs = [
|
||||
(key, str(item))
|
||||
for key, value in data.items()
|
||||
for item in (value if isinstance(value, (list, tuple)) else [value])
|
||||
]
|
||||
body = urllib.parse.urlencode(pairs).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
self.base + path,
|
||||
data=body,
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Origin": self.base,
|
||||
"Referer": self.base + path,
|
||||
},
|
||||
)
|
||||
return self._open(request)
|
||||
|
||||
def post_file(
|
||||
self, path: str, field: str, filename: str, content: bytes, extra: dict[str, str]
|
||||
) -> Response:
|
||||
boundary = "----tesm-e2e-boundary-3f88"
|
||||
parts: list[bytes] = []
|
||||
for key, value in extra.items():
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"\r\n\r\n'
|
||||
f"{value}\r\n".encode()
|
||||
)
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{field}"; '
|
||||
f'filename="{filename}"\r\nContent-Type: application/json\r\n\r\n'.encode()
|
||||
+ content
|
||||
+ b"\r\n"
|
||||
)
|
||||
parts.append(f"--{boundary}--\r\n".encode())
|
||||
request = urllib.request.Request(
|
||||
self.base + path,
|
||||
data=b"".join(parts),
|
||||
headers={
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
"Origin": self.base,
|
||||
"Referer": self.base + path,
|
||||
},
|
||||
)
|
||||
return self._open(request)
|
||||
|
||||
def token(self, path: str) -> str:
|
||||
body = self.text(path)
|
||||
match = CSRF_RE.search(body) or BODY_CSRF_RE.search(body)
|
||||
if not match:
|
||||
raise AssertionError(f"{self.label}: kein CSRF-Token auf {path}")
|
||||
return match.group(1)
|
||||
|
||||
def login(self, username: str, password: str) -> bool:
|
||||
token = self.token("/login")
|
||||
response = self.post(
|
||||
"/login", {"username": username, "password": password, "csrf_token": token}
|
||||
)
|
||||
return response.status == 200 and "/login" not in response.url
|
||||
|
||||
|
||||
def main(
|
||||
server_base: str,
|
||||
client_base: str,
|
||||
username: str,
|
||||
password: str,
|
||||
endpoint: str = "",
|
||||
) -> int:
|
||||
# Der Endpunkt, den TESM spaeter anruft, muss nicht die Adresse sein, unter
|
||||
# der ein Mensch den Lizenzserver bedient. Auf einem Host, der beide
|
||||
# Anwendungen traegt, ist der direkte Weg ueber die Loopback-Adresse der
|
||||
# ehrlichere -- er umgeht Namensaufloesung und Zertifikatspruefung, die
|
||||
# zwischen zwei Diensten derselben Maschine nichts beitragen.
|
||||
endpoint = (endpoint or server_base).rstrip("/")
|
||||
server = Client(server_base, "Lizenzserver")
|
||||
tesm = Client(client_base, "TESM")
|
||||
print(f"\n== Lizenz-Lebenszyklus: {server_base} -> {client_base} ==\n")
|
||||
|
||||
print("Anmeldung:")
|
||||
check("Lizenzserver", server.login(username, password), "fehlgeschlagen")
|
||||
check("TESM", tesm.login(username, password), "fehlgeschlagen")
|
||||
if FAILED:
|
||||
print("\nOhne Anmeldung ist der Rest sinnlos -- Abbruch.")
|
||||
return 1
|
||||
|
||||
# -- Anbieterdaten: der Endpunkt landet spaeter in jeder Lizenz -----------
|
||||
print("\nAnbieterdaten und Endpunkt:")
|
||||
token = server.token("/einstellungen/anbieter")
|
||||
response = server.post(
|
||||
"/einstellungen/anbieter",
|
||||
{
|
||||
"csrf_token": token,
|
||||
"vendor_name": "WiS GmbH",
|
||||
"vendor_email": "lizenzen@wis.invalid",
|
||||
"vendor_phone": "+49 000 000000",
|
||||
"vendor_address": "Musterweg 1, 12345 Musterstadt",
|
||||
"master_endpoint": endpoint,
|
||||
},
|
||||
)
|
||||
check(
|
||||
"Anbieterdaten gespeichert",
|
||||
"gespeichert" in response.text or "Keine Aenderungen" in response.text,
|
||||
response.text[:150],
|
||||
)
|
||||
token = server.token("/einstellungen/anbieter")
|
||||
response = server.post(
|
||||
"/einstellungen/anbieter", {"csrf_token": token, "master_endpoint": "ftp://falsch"}
|
||||
)
|
||||
check("Ungueltiger Endpunkt abgelehnt", "muss mit http" in response.text, "keine Meldung")
|
||||
|
||||
# -- Kunde ---------------------------------------------------------------
|
||||
print("\nKunde:")
|
||||
token = server.token("/kunden/")
|
||||
response = server.post(
|
||||
"/kunden/neu",
|
||||
{
|
||||
"csrf_token": token,
|
||||
"name": "Beispiel AG",
|
||||
"contact_email": "it@beispiel.invalid",
|
||||
"contact_person": "Frau Muster",
|
||||
"street": "Hauptstrasse",
|
||||
"house_number": "7",
|
||||
"postal_code": "12345",
|
||||
"city": "Musterstadt",
|
||||
},
|
||||
)
|
||||
check("Kunde angelegt", "angelegt" in response.text, response.text[:150])
|
||||
match = re.search(r"/kunden/(\d+)$", response.url)
|
||||
customer_id = match.group(1) if match else ""
|
||||
check("Kunden-Nummer erhalten", bool(customer_id), response.url)
|
||||
|
||||
# -- Ticket ---------------------------------------------------------------
|
||||
print("\nTicket:")
|
||||
token = server.token("/tickets/neu")
|
||||
response = server.post(
|
||||
"/tickets/neu",
|
||||
{
|
||||
"csrf_token": token,
|
||||
"customer_id": customer_id,
|
||||
"type": "enterprise",
|
||||
"modules": ["dhcp", "fileshare", "maintenance"],
|
||||
"valid_days": "365",
|
||||
"role": "customer",
|
||||
"notes": "E2E-Test",
|
||||
},
|
||||
)
|
||||
check("Ticket angelegt", "angelegt" in response.text, response.text[:150])
|
||||
match = re.search(r"/tickets/([^/]+)$", response.url)
|
||||
ticket_id = match.group(1) if match else ""
|
||||
check("Ticket-Kennung erhalten", bool(ticket_id), response.url)
|
||||
|
||||
token = server.token("/tickets/neu")
|
||||
response = server.post(
|
||||
"/tickets/neu",
|
||||
{
|
||||
"csrf_token": token,
|
||||
"customer_id": customer_id,
|
||||
"type": "trial",
|
||||
"valid_days": "30",
|
||||
"lifetime": "1",
|
||||
"role": "customer",
|
||||
},
|
||||
)
|
||||
check(
|
||||
"Testlizenz kann nicht unbefristet sein",
|
||||
"nicht unbefristet" in response.text,
|
||||
"Regel greift nicht",
|
||||
)
|
||||
|
||||
# -- Lizenz ausstellen ----------------------------------------------------
|
||||
print("\nLizenz ausstellen:")
|
||||
token = server.token(f"/tickets/{ticket_id}")
|
||||
response = server.post(f"/lizenzen/aus-ticket/{ticket_id}", {"csrf_token": token})
|
||||
check("Lizenz ausgestellt", "ausgestellt" in response.text, response.text[:200])
|
||||
match = re.search(r"/lizenzen/([0-9a-f-]{36})$", response.url)
|
||||
license_id = match.group(1) if match else ""
|
||||
check("Lizenz-Kennung erhalten", bool(license_id), response.url)
|
||||
|
||||
token = server.token(f"/tickets/{ticket_id}")
|
||||
response = server.post(f"/lizenzen/aus-ticket/{ticket_id}", {"csrf_token": token})
|
||||
check(
|
||||
"Doppelklick stellt keine zweite Lizenz aus",
|
||||
"bereits eine Lizenz offen" in response.text,
|
||||
"zweite Lizenz entstanden",
|
||||
)
|
||||
|
||||
# -- Bundle herunterladen -------------------------------------------------
|
||||
print("\nAuslieferung:")
|
||||
response = server.get(f"/lizenzen/{license_id}/datei")
|
||||
check("Bundle heruntergeladen", response.status == 200, f"HTTP {response.status}")
|
||||
check(
|
||||
"Als Anhang ausgeliefert",
|
||||
"attachment" in response.headers.get("Content-Disposition", ""),
|
||||
response.headers.get("Content-Disposition", ""),
|
||||
)
|
||||
check(
|
||||
"Nicht zwischenspeicherbar",
|
||||
"no-store" in response.headers.get("Cache-Control", ""),
|
||||
response.headers.get("Cache-Control", ""),
|
||||
)
|
||||
bundle_bytes = response.raw
|
||||
try:
|
||||
bundle = json.loads(bundle_bytes.decode("utf-8"))
|
||||
except ValueError:
|
||||
check("Bundle ist gueltiges JSON", False, bundle_bytes[:120].decode("latin1"))
|
||||
return 1
|
||||
check("Bundle ist gueltiges JSON", True)
|
||||
check(
|
||||
"Dokument und privater Clientschluessel enthalten",
|
||||
"license" in bundle and "client_key" in bundle,
|
||||
str(sorted(bundle))[:100],
|
||||
)
|
||||
document = bundle.get("license", {})
|
||||
check(
|
||||
"Endpunkt eingebettet",
|
||||
document.get("master_endpoint") == endpoint,
|
||||
str(document.get("master_endpoint")),
|
||||
)
|
||||
check(
|
||||
"Alle drei Module enthalten",
|
||||
set(document.get("modules", [])) == {"dhcp", "fileshare", "maintenance"},
|
||||
str(document.get("modules")),
|
||||
)
|
||||
check("Rolle ist customer", document.get("role") == "customer", str(document.get("role")))
|
||||
|
||||
body = server.text(f"/lizenzen/{license_id}")
|
||||
check("Zustellprotokoll vermerkt den Download", "ownload" in body, "kein Eintrag")
|
||||
|
||||
# -- Einspielen in TESM ---------------------------------------------------
|
||||
print("\nEinspielen in TESM:")
|
||||
body = tesm.text("/lizenz/")
|
||||
if "keine Lizenz installiert" in body:
|
||||
check("Vorher keine Lizenz installiert", True)
|
||||
else:
|
||||
# Bei einem wiederholten Lauf liegt noch die Lizenz des Vorlaufs. Das ist
|
||||
# kein Fehler -- das Einspielen ersetzt sie, und genau das wird gleich
|
||||
# geprueft.
|
||||
print(" [info] Es liegt bereits eine Lizenz -- wiederholter Lauf, wird ersetzt.")
|
||||
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post_file(
|
||||
"/lizenz/einspielen", "license_file", "lizenz.json", bundle_bytes, {"csrf_token": token}
|
||||
)
|
||||
check("Lizenz eingespielt", "eingespielt" in response.text, response.text[:250])
|
||||
check(
|
||||
"Aussteller beim ersten Mal gepinnt",
|
||||
"Aussteller gepinnt" in response.text,
|
||||
"kein Pinning erkennbar",
|
||||
)
|
||||
check("Noch nicht aktiviert", "nicht aktiviert" in response.text, "Zustand unerwartet")
|
||||
|
||||
print("\nModule vor der Aktivierung:")
|
||||
for path in ("/dhcp/", "/wartung/", "/dateifreigaben/"):
|
||||
check(f"{path} gesperrt", tesm.get(path).status == 404, "erreichbar")
|
||||
|
||||
# -- Manipuliertes Bundle -------------------------------------------------
|
||||
print("\nManipuliertes Bundle:")
|
||||
tampered = json.loads(bundle_bytes.decode("utf-8"))
|
||||
tampered["license"]["modules"] = ["dhcp", "fileshare", "maintenance", "erfunden"]
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post_file(
|
||||
"/lizenz/einspielen",
|
||||
"license_file",
|
||||
"boese.json",
|
||||
json.dumps(tampered).encode("utf-8"),
|
||||
{"csrf_token": token},
|
||||
)
|
||||
check(
|
||||
"Veraenderte Lizenz wird abgewiesen",
|
||||
"eingespielt" not in response.text,
|
||||
"Manipulation akzeptiert!",
|
||||
)
|
||||
|
||||
# -- Aktivierung ----------------------------------------------------------
|
||||
print("\nAktivierung (online, echter HTTP-Aufruf an den Lizenzserver):")
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/aktivieren", {"csrf_token": token})
|
||||
check("Aktivierung erfolgreich", "jetzt aktiv" in response.text, response.text[:300])
|
||||
check("Zustand aktiviert", "nicht aktiviert" not in response.text, "Zustand unerwartet")
|
||||
|
||||
print("\nModule nach der Aktivierung:")
|
||||
for path, needle in (
|
||||
("/dhcp/", "DHCP"),
|
||||
("/wartung/", "Wartung"),
|
||||
("/dateifreigaben/", "Dateifreigaben"),
|
||||
):
|
||||
response = tesm.get(path)
|
||||
check(
|
||||
f"{path} freigeschaltet",
|
||||
response.status == 200 and needle in response.text,
|
||||
f"HTTP {response.status}",
|
||||
)
|
||||
|
||||
print("\nSicht des Lizenzservers:")
|
||||
body = server.text(f"/lizenzen/{license_id}")
|
||||
check("Maschine gebunden", "noch nicht gebunden" not in body, "kein Fingerprint")
|
||||
check("Aktivierungszeitpunkt vermerkt", "Aktiviert" in body, "fehlt")
|
||||
|
||||
# -- Heartbeat ------------------------------------------------------------
|
||||
print("\nHeartbeat:")
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/heartbeat", {"csrf_token": token})
|
||||
check("Heartbeat bestaetigt", "bestaetigt" in response.text, response.text[:250])
|
||||
|
||||
# -- Neue Revision: Modul entziehen ---------------------------------------
|
||||
print("\nNeue Revision (Modul entziehen):")
|
||||
token = server.token(f"/lizenzen/{license_id}")
|
||||
response = server.post(
|
||||
f"/lizenzen/{license_id}/neu-signieren",
|
||||
{"csrf_token": token, "modules": ["dhcp"], "extend_days": ""},
|
||||
)
|
||||
check("Revision signiert", "Revision" in response.text, response.text[:250])
|
||||
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/heartbeat", {"csrf_token": token})
|
||||
check("Heartbeat holt die Revision", "bestaetigt" in response.text, response.text[:200])
|
||||
check("Entzogenes Modul gesperrt", tesm.get("/wartung/").status == 404, "weiter offen")
|
||||
check("Verbliebenes Modul offen", tesm.get("/dhcp/").status == 200, "gesperrt")
|
||||
|
||||
# -- Deaktivierung und erneute Aktivierung --------------------------------
|
||||
print("\nDeaktivierung (Systemwechsel):")
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/deaktivieren", {"csrf_token": token})
|
||||
check("Deaktivierung erfolgreich", "wurde deaktiviert" in response.text, response.text[:250])
|
||||
check("Module wieder gesperrt", tesm.get("/dhcp/").status == 404, "weiter offen")
|
||||
body = server.text(f"/lizenzen/{license_id}")
|
||||
check("Server kennt die Deaktivierung", "deaktiviert" in body, "unerwartet")
|
||||
|
||||
print("\nErneute Aktivierung derselben Maschine:")
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/aktivieren", {"csrf_token": token})
|
||||
check("Wieder aktiv", "jetzt aktiv" in response.text, response.text[:250])
|
||||
check("Modul wieder offen", tesm.get("/dhcp/").status == 200, "gesperrt")
|
||||
|
||||
# -- Widerruf -------------------------------------------------------------
|
||||
print("\nWiderruf durch den Anbieter:")
|
||||
token = server.token(f"/lizenzen/{license_id}")
|
||||
response = server.post(
|
||||
f"/lizenzen/{license_id}/widerrufen", {"csrf_token": token, "reason": "E2E-Test"}
|
||||
)
|
||||
check("Widerruf gespeichert", "widerrufen" in response.text, response.text[:250])
|
||||
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/heartbeat", {"csrf_token": token})
|
||||
check("Client erfaehrt den Widerruf", "widerrufen" in response.text, response.text[:250])
|
||||
check("Module nach Widerruf gesperrt", tesm.get("/dhcp/").status == 404, "weiter offen")
|
||||
check("Lizenzseite bleibt erreichbar", tesm.get("/lizenz/").status == 200, "gesperrt")
|
||||
|
||||
# -- Aufraeumen -------------------------------------------------------
|
||||
# Der Test endet mit einem Widerruf -- danach sind DHCP, Wartung und
|
||||
# Dateifreigaben gesperrt. Eine Pruefung, die die Installation unbrauchbar
|
||||
# zuruecklaesst, ist keine gute Pruefung: wer danach die Wartung sucht,
|
||||
# findet einen 404 und haelt ihn fuer einen Fehler. Also wird zum Schluss
|
||||
# eine frische Lizenz ausgestellt und aktiviert.
|
||||
print("\nAufraeumen -- Installation wieder betriebsbereit machen:")
|
||||
token = server.token(f"/tickets/{ticket_id}")
|
||||
response = server.post(f"/lizenzen/aus-ticket/{ticket_id}", {"csrf_token": token})
|
||||
check("Ersatzlizenz ausgestellt", "ausgestellt" in response.text, response.text[:200])
|
||||
match = re.search(r"/lizenzen/([0-9a-f-]{36})$", response.url)
|
||||
replacement_id = match.group(1) if match else ""
|
||||
check("Kennung der Ersatzlizenz", bool(replacement_id), response.url)
|
||||
|
||||
response = server.get(f"/lizenzen/{replacement_id}/datei")
|
||||
check("Ersatzbundle heruntergeladen", response.status == 200, f"HTTP {response.status}")
|
||||
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post_file(
|
||||
"/lizenz/einspielen", "license_file", "lizenz.json", response.raw, {"csrf_token": token}
|
||||
)
|
||||
check("Ersatzlizenz eingespielt", "eingespielt" in response.text, response.text[:200])
|
||||
|
||||
token = tesm.token("/lizenz/")
|
||||
response = tesm.post("/lizenz/aktivieren", {"csrf_token": token})
|
||||
check("Ersatzlizenz aktiv", "jetzt aktiv" in response.text, response.text[:250])
|
||||
for path in ("/dhcp/", "/wartung/", "/dateifreigaben/"):
|
||||
check(f"{path} wieder erreichbar", tesm.get(path).status == 200, "weiter gesperrt")
|
||||
|
||||
# -- Protokolle -----------------------------------------------------------
|
||||
print("\nProtokolle und Diagnose:")
|
||||
body = server.text("/protokolle/aenderungen")
|
||||
for action in ("license.issued", "license.downloaded", "license.resigned", "license.revoked"):
|
||||
check(f"Eintrag {action}", action in body, "fehlt")
|
||||
check(
|
||||
"Diagnose Lizenzserver: Kette unversehrt",
|
||||
"unversehrt" in server.text("/diagnose/"),
|
||||
"gebrochen",
|
||||
)
|
||||
|
||||
body = tesm.text("/protokolle/aenderungen")
|
||||
for action in ("license.activate", "license.deactivate", "license.heartbeat"):
|
||||
check(f"TESM-Eintrag {action}", action in body, "fehlt")
|
||||
check("Diagnose TESM: Kette unversehrt", "unversehrt" in tesm.text("/diagnose/"), "gebrochen")
|
||||
|
||||
print(f"\n== Ergebnis: {len(PASSED)} bestanden, {len(FAILED)} fehlgeschlagen ==")
|
||||
for entry in FAILED:
|
||||
print(f" FEHLER: {entry}")
|
||||
return 1 if FAILED else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) not in (5, 6):
|
||||
print(__doc__)
|
||||
raise SystemExit(2)
|
||||
raise SystemExit(main(*sys.argv[1:]))
|
||||
Reference in New Issue
Block a user