Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f481a91773 | ||
|
|
970f45cdf7 | ||
|
|
c9ceb871b6 | ||
|
|
37c508297f | ||
|
|
13bf07fecf |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.33"
|
version = "2.8.38"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ from tesm_core.auth.models import (
|
|||||||
from tesm_core.auth.service import client_ip
|
from tesm_core.auth.service import client_ip
|
||||||
from tesm_core.db import Database
|
from tesm_core.db import Database
|
||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
from tesm_core.licensing_client import LICENSED_FEATURES, LicenseError
|
from tesm_core.licensing_client import (
|
||||||
|
ABFRAGE_WARNSCHWELLE_TAGE,
|
||||||
|
LICENSED_FEATURES,
|
||||||
|
LicenseError,
|
||||||
|
)
|
||||||
|
|
||||||
import tesm_licensing as lic
|
import tesm_licensing as lic
|
||||||
|
|
||||||
@@ -50,8 +54,29 @@ def overview():
|
|||||||
manager = extension.license
|
manager = extension.license
|
||||||
state = manager.reload(conn) if manager else None
|
state = manager.reload(conn) if manager else None
|
||||||
pending = manager._pending(conn) if manager else {} # noqa: SLF001 - eigenes Paket
|
pending = manager._pending(conn) if manager else {} # noqa: SLF001 - eigenes Paket
|
||||||
|
# Der Befehl, den ein Agent auf dieser Maschine ausfuehrt. Zusammengesetzt
|
||||||
|
# aus dem, was hier wirklich installiert ist -- der Shim traegt den Namen
|
||||||
|
# der Instanz, und der Dienstbenutzer auch. Abgeschrieben aus der Doku
|
||||||
|
# waere er auf einer benannten Instanz schlicht falsch.
|
||||||
|
werkzeug = f"/usr/local/bin/{extension.config.site_key}-admin"
|
||||||
|
# Der Dienstbenutzer traegt den Namen der **Anwendung**, nicht den der
|
||||||
|
# Instanz: install.sh setzt SERVICE_USER="$APP". Auf "tesm-test" heisst er
|
||||||
|
# also "tesm" -- und auf dem Lizenzserver "tesm-license". Aus dem
|
||||||
|
# Instanznamen ableiten waere genau dort falsch.
|
||||||
|
dienstbenutzer = extension.config.app_key
|
||||||
|
rmm_befehl = f"sudo -u {dienstbenutzer} {werkzeug} license"
|
||||||
return render_template(
|
return render_template(
|
||||||
"tesm/license.html",
|
"tesm/license.html",
|
||||||
|
rmm_befehl=rmm_befehl,
|
||||||
|
rmm_befehl_tage=f"{rmm_befehl} --days",
|
||||||
|
rmm_warnschwelle=ABFRAGE_WARNSCHWELLE_TAGE,
|
||||||
|
rmm_powershell=(
|
||||||
|
f'$j = (& {rmm_befehl} 2>$null) | ConvertFrom-Json' + chr(10)
|
||||||
|
+ 'if (-not $j.valid) { exit 2 }' + chr(10)
|
||||||
|
+ 'if ($j.lifetime) { exit 0 }' + chr(10)
|
||||||
|
+ f'if ($j.days_left -le {ABFRAGE_WARNSCHWELLE_TAGE}) ' + '{ exit 1 }' + chr(10)
|
||||||
|
+ 'exit 0'
|
||||||
|
),
|
||||||
state=state,
|
state=state,
|
||||||
pending=pending,
|
pending=pending,
|
||||||
server_certificate=extension.settings.get(conn, "license_server_ca") or "",
|
server_certificate=extension.settings.get(conn, "license_server_ca") or "",
|
||||||
|
|||||||
@@ -110,6 +110,36 @@ def cmd_create_admin(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_license(args: argparse.Namespace) -> int:
|
||||||
|
"""Die Lizenz als Auskunft fuer ein Ueberwachungswerkzeug.
|
||||||
|
|
||||||
|
Gedacht fuer einen RMM-Agenten, der auf dieser Maschine laeuft: kein Netz,
|
||||||
|
kein Token, keine Anmeldung. Die Antwort steht auf **stdout**, alles andere
|
||||||
|
(Protokollzeilen des Starts) auf stderr -- ein ``| ConvertFrom-Json`` soll
|
||||||
|
nicht daran scheitern, dass eine Migration etwas erzaehlt hat.
|
||||||
|
|
||||||
|
Der Ausstiegscode traegt die Aussage: 0 heil, 1 laeuft bald ab, 2
|
||||||
|
ungueltig. Das ist die Sprache, in der ein RMM Alarm schlaegt -- Text
|
||||||
|
auszuwerten waere der Umweg.
|
||||||
|
"""
|
||||||
|
from tesm_core.licensing_client import abfrage_ausstiegscode, zustand_als_auskunft
|
||||||
|
|
||||||
|
app, extension = _app_and_core()
|
||||||
|
if extension.license is None:
|
||||||
|
print("{}")
|
||||||
|
return 2
|
||||||
|
with app.app_context(), extension.database.session() as conn:
|
||||||
|
auskunft = zustand_als_auskunft(extension.license.reload(conn))
|
||||||
|
if getattr(args, "days", False):
|
||||||
|
# Eine Zahl, sonst nichts -- fuer Werkzeuge, die keinen JSON-Parser
|
||||||
|
# mitbringen. Unbefristet hat keine Zahl und sagt das auch.
|
||||||
|
tage = auskunft.get("days_left")
|
||||||
|
print("unbefristet" if auskunft.get("lifetime") else ("" if tage is None else int(tage)))
|
||||||
|
else:
|
||||||
|
print(json.dumps(auskunft, ensure_ascii=False, sort_keys=True))
|
||||||
|
return abfrage_ausstiegscode(auskunft)
|
||||||
|
|
||||||
|
|
||||||
def cmd_status(args: argparse.Namespace) -> int:
|
def cmd_status(args: argparse.Namespace) -> int:
|
||||||
app, extension = _app_and_core()
|
app, extension = _app_and_core()
|
||||||
with app.app_context(), extension.database.session() as conn:
|
with app.app_context(), extension.database.session() as conn:
|
||||||
@@ -552,6 +582,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
status.add_argument("--json", action="store_true")
|
status.add_argument("--json", action="store_true")
|
||||||
status.set_defaults(func=cmd_status)
|
status.set_defaults(func=cmd_status)
|
||||||
|
|
||||||
|
lizenz = sub.add_parser(
|
||||||
|
"license", help="Lizenzzustand als JSON ausgeben (für Überwachungswerkzeuge)"
|
||||||
|
)
|
||||||
|
lizenz.add_argument(
|
||||||
|
"--days", action="store_true", help="Nur die Restlaufzeit in Tagen ausgeben"
|
||||||
|
)
|
||||||
|
lizenz.set_defaults(func=cmd_license)
|
||||||
|
|
||||||
house = sub.add_parser("housekeeping", help="Sitzungen, Ratenereignisse, Protokolle aufräumen")
|
house = sub.add_parser("housekeeping", help="Sitzungen, Ratenereignisse, Protokolle aufräumen")
|
||||||
house.set_defaults(func=cmd_housekeeping)
|
house.set_defaults(func=cmd_housekeeping)
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{# Fuer ein Ueberwachungswerkzeug, das auf dieser Maschine laeuft. Bewusst
|
||||||
|
kein HTTP-Endpunkt mit Token: der Agent sitzt ohnehin hier, und was ohne
|
||||||
|
Netz auskommt, kann auch niemand aus dem Netz abfragen. #}
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">{{ icon('terminal', size=17) }} Abfrage durch ein RMM</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Ein Agent auf dieser Maschine liest die Restlaufzeit ohne Anmeldung und ohne Netz.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--sm btn--ghost" type="button" data-copy="#rmm-json"
|
||||||
|
data-copy-message="Befehl kopiert.">{{ icon('copy', size=14) }} Kopieren</button>
|
||||||
|
</div>
|
||||||
|
<div class="card__body stack stack--tight">
|
||||||
|
<pre class="code-block code-block--wrap" id="rmm-json">{{ rmm_befehl }}</pre>
|
||||||
|
<p class="fs-xs faint">
|
||||||
|
Antwortet mit einer Zeile JSON auf <strong>stdout</strong>
|
||||||
|
(<code>valid</code>, <code>days_left</code>, <code>expires_at</code>,
|
||||||
|
<code>lifetime</code>, <code>modules</code>). Der <strong>Ausstiegscode</strong> trägt
|
||||||
|
die Aussage: <strong>0</strong> in Ordnung, <strong>1</strong> läuft in
|
||||||
|
{{ rmm_warnschwelle }} Tagen oder weniger ab, <strong>2</strong> ungültig oder nicht
|
||||||
|
vorhanden. Meldungen des Starts gehen nach stderr und stören das Auswerten nicht.
|
||||||
|
</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="fs-xs faint">Nur die Restlaufzeit, ohne JSON-Auswertung:</div>
|
||||||
|
<pre class="code-block code-block--wrap" id="rmm-tage">{{ rmm_befehl_tage }}</pre>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="fs-xs faint">Beispiel für einen Agenten mit PowerShell:</div>
|
||||||
|
<pre class="code-block code-block--wrap">{{ rmm_powershell }}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
+23
-1
@@ -26,6 +26,23 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
APP=""
|
APP=""
|
||||||
INSTANCE=""
|
INSTANCE=""
|
||||||
#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die
|
#: Verzeichnis der Umgebungsdateien. Ueberschreibbar, damit sich die
|
||||||
@@ -422,7 +439,12 @@ step "Python-Umgebung"
|
|||||||
# dasselbe laeuft als ExecStartPre vor jedem Dienststart. Zwei getrennte
|
# dasselbe laeuft als ExecStartPre vor jedem Dienststart. Zwei getrennte
|
||||||
# Paketlisten waeren zwei Listen, die auseinanderlaufen: die Reparatur baute
|
# Paketlisten waeren zwei Listen, die auseinanderlaufen: die Reparatur baute
|
||||||
# dann eine Umgebung, die nicht mehr der Installation entspricht.
|
# dann eine Umgebung, die nicht mehr der Installation entspricht.
|
||||||
"$ROOT/deploy/tesm-venv" "$ROOT" "$APP" --immer
|
# Ueber "bash" und nicht als Programm: Dateien im Baum tragen kein
|
||||||
|
# Ausfuehrungsrecht -- git kennt auf einer Windows-Buildmaschine keines, und
|
||||||
|
# install.sh, tesm-helper und tesm-motd werden aus demselben Grund ebenso
|
||||||
|
# aufgerufen oder mit ausdruecklichem Modus installiert. Ohne das brach die
|
||||||
|
# Aktualisierung mit "Permission denied" ab (und rollte zurueck).
|
||||||
|
bash "$ROOT/deploy/tesm-venv" "$ROOT" "$APP" --immer
|
||||||
info "Abhaengigkeiten installiert"
|
info "Abhaengigkeiten installiert"
|
||||||
|
|
||||||
# -- Statische Dateien fuer nginx --------------------------------------------
|
# -- Statische Dateien fuer nginx --------------------------------------------
|
||||||
|
|||||||
@@ -28,6 +28,18 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# **Der venv muss fuer den Dienstbenutzer lesbar sein.**
|
||||||
|
#
|
||||||
|
# Dieses Programm laeuft als ExecStartPre der Dienst-Unit -- und erbt damit
|
||||||
|
# deren UMask=0027. Alles, was es anlegt, gehoerte root und waere 0750: der
|
||||||
|
# Dienst selbst laeuft unprivilegiert und koennte sein eigenes site-packages
|
||||||
|
# nicht einmal betreten. Gemessen: nach der Reparatur ging "import gunicorn"
|
||||||
|
# als root, als Benutzer tesm nicht -- die Reparatur baute etwas, das genau
|
||||||
|
# der Prozess nicht benutzen konnte, fuer den sie gedacht war.
|
||||||
|
#
|
||||||
|
# Im venv liegt nichts Geheimes; Schluessel und Datenbank stehen woanders.
|
||||||
|
umask 022
|
||||||
|
|
||||||
ROOT="${1:-}"
|
ROOT="${1:-}"
|
||||||
APP="${2:-}"
|
APP="${2:-}"
|
||||||
MODUS="reparieren"
|
MODUS="reparieren"
|
||||||
@@ -130,6 +142,11 @@ if ! bestuecken; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Und noch einmal ausdruecklich, fuer den Fall, dass ein frueherer Lauf mit
|
||||||
|
# strengerer Maske gebaut hat: was hier liegt, muss lesbar sein. Idempotent
|
||||||
|
# und in einer Sekunde durch.
|
||||||
|
chmod -R a+rX "$VENV" 2>/dev/null || true
|
||||||
|
|
||||||
[[ "$MODUS" == "immer" ]] || melden "INFO" "Python-Umgebung von $NAME ist wieder vollstaendig."
|
[[ "$MODUS" == "immer" ]] || melden "INFO" "Python-Umgebung von $NAME ist wieder vollstaendig."
|
||||||
rm -f "$MERKER" 2>/dev/null || true
|
rm -f "$MERKER" 2>/dev/null || true
|
||||||
exit 0
|
exit 0
|
||||||
@@ -29,6 +29,23 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
APP=""
|
APP=""
|
||||||
INSTANCE=""
|
INSTANCE=""
|
||||||
PURGE=0
|
PURGE=0
|
||||||
|
|||||||
@@ -18,6 +18,23 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in
|
||||||
|
# die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt
|
||||||
|
# LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel
|
||||||
|
# nicht erzeugt, warnt **jede neu gestartete Shell**:
|
||||||
|
#
|
||||||
|
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
|
||||||
|
#
|
||||||
|
# Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist
|
||||||
|
# keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier
|
||||||
|
# laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders
|
||||||
|
# klingt, liest sich schlecht.
|
||||||
|
if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then
|
||||||
|
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||||
|
else
|
||||||
|
export LC_ALL=C LANG=C
|
||||||
|
fi
|
||||||
|
|
||||||
APP="${1:-}"
|
APP="${1:-}"
|
||||||
TAG="${2:-latest}"
|
TAG="${2:-latest}"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.18"
|
version = "2.8.20"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -162,6 +162,67 @@ class LicenseState:
|
|||||||
return dict(self.status.vendor) if self.status else {}
|
return dict(self.status.vendor) if self.status else {}
|
||||||
|
|
||||||
|
|
||||||
|
#: Ausstiegscodes der Abfrage -- danach schlaegt ein RMM Alarm, nicht nach Text.
|
||||||
|
ABFRAGE_OK = 0
|
||||||
|
ABFRAGE_LAEUFT_AB = 1
|
||||||
|
ABFRAGE_UNGUELTIG = 2
|
||||||
|
|
||||||
|
#: Ab hier gilt eine Lizenz als "laeuft bald ab". Dieselbe Zahl wie in der
|
||||||
|
#: Oberflaeche -- zwei Schwellen fuer dieselbe Frage waeren eine zu viel.
|
||||||
|
ABFRAGE_WARNSCHWELLE_TAGE = 30
|
||||||
|
|
||||||
|
|
||||||
|
def zustand_als_auskunft(state: "LicenseState") -> dict[str, Any]:
|
||||||
|
"""Der Lizenzzustand als schlichtes Woerterbuch -- fuer JSON und Skripte.
|
||||||
|
|
||||||
|
Gedacht fuer ein RMM, das auf der Maschine selbst laeuft: kein Netz, kein
|
||||||
|
Token, keine Anmeldung. Was hier steht, beantwortet die eine Frage, die ein
|
||||||
|
Kunde wirklich hat -- wie lange laeuft die Lizenz noch --, und nichts
|
||||||
|
darueber hinaus: keine Schluessel, keine Kennungen anderer Installationen.
|
||||||
|
"""
|
||||||
|
status = state.status
|
||||||
|
return {
|
||||||
|
"valid": bool(state.valid),
|
||||||
|
"activated": bool(state.activated),
|
||||||
|
"type": str(status.type) if status else "",
|
||||||
|
"role": state.role,
|
||||||
|
"lifetime": bool(status.lifetime) if status else False,
|
||||||
|
"expires_at": lic.iso(status.expires_at) if status and status.expires_at else "",
|
||||||
|
# Bei einer unbefristeten Lizenz ist die Zahl bewusst ``null`` und
|
||||||
|
# keine grosse Zahl: "noch 99999 Tage" waere eine Behauptung.
|
||||||
|
"days_left": (status.days_left if status and not status.lifetime else None),
|
||||||
|
"expiring_soon": bool(status.expiring_soon) if status else False,
|
||||||
|
"grace_active": bool(status.grace_active) if status else False,
|
||||||
|
# Drei Angaben, weil zwei davon verschieden sein koennen und die
|
||||||
|
# Verwechslung teuer ist: "modules" ist, was die **Lizenz** freigibt --
|
||||||
|
# dieselbe Liste, die der Lizenzserver zeigt. Was davon vor Ort
|
||||||
|
# abgeschaltet wurde, steht daneben.
|
||||||
|
#
|
||||||
|
# Vorher stand hier allein das Laufende. Beim Kunden fehlte darin
|
||||||
|
# "dhcp", waehrend die Lizenzseite es auswies -- es war lediglich unter
|
||||||
|
# Verwaltung -> System abgeschaltet. Aus der Antwort ging das nicht
|
||||||
|
# hervor, und damit sah es nach einem Lizenzproblem aus.
|
||||||
|
"modules": sorted(state.modules),
|
||||||
|
"modules_active": sorted(state.modules_in_use),
|
||||||
|
"modules_off": sorted(state.modules_off),
|
||||||
|
"customer": state.customer_name,
|
||||||
|
"last_heartbeat_at": state.last_heartbeat_at,
|
||||||
|
"error": state.error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def abfrage_ausstiegscode(auskunft: dict[str, Any]) -> int:
|
||||||
|
"""0 heil, 1 laeuft bald ab, 2 ungueltig -- die Sprache eines RMM."""
|
||||||
|
if not auskunft.get("valid"):
|
||||||
|
return ABFRAGE_UNGUELTIG
|
||||||
|
if auskunft.get("lifetime"):
|
||||||
|
return ABFRAGE_OK
|
||||||
|
tage = auskunft.get("days_left")
|
||||||
|
if tage is None or int(tage) <= ABFRAGE_WARNSCHWELLE_TAGE:
|
||||||
|
return ABFRAGE_LAEUFT_AB
|
||||||
|
return ABFRAGE_OK
|
||||||
|
|
||||||
|
|
||||||
class LicenseManager:
|
class LicenseManager:
|
||||||
"""Haelt den Lizenzzustand und fuehrt die Handshakes durch."""
|
"""Haelt den Lizenzzustand und fuehrt die Handshakes durch."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.7.7"
|
version = "2.7.8"
|
||||||
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"]
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .license import (
|
|||||||
key_id,
|
key_id,
|
||||||
license_status,
|
license_status,
|
||||||
parse_iso,
|
parse_iso,
|
||||||
|
tage_bis,
|
||||||
split_bundle,
|
split_bundle,
|
||||||
utcnow,
|
utcnow,
|
||||||
verify_license,
|
verify_license,
|
||||||
@@ -103,6 +104,7 @@ __all__ = [
|
|||||||
"machine_identity",
|
"machine_identity",
|
||||||
"new_nonce",
|
"new_nonce",
|
||||||
"parse_iso",
|
"parse_iso",
|
||||||
|
"tage_bis",
|
||||||
"sign",
|
"sign",
|
||||||
"split_bundle",
|
"split_bundle",
|
||||||
"system_fingerprint",
|
"system_fingerprint",
|
||||||
|
|||||||
@@ -221,6 +221,27 @@ def verify_license(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tage_bis(expires_at: datetime, *, now: datetime | None = None) -> int:
|
||||||
|
"""Restlaufzeit in **angefangenen** Tagen.
|
||||||
|
|
||||||
|
Aufgerundet, und das ist eine Entscheidung: eine Lizenz, die in zwei
|
||||||
|
Stunden ablaeuft, hat "1 Tag" und nicht "0". Null bedeutet hier
|
||||||
|
ausschliesslich *abgelaufen* -- eine gueltige Lizenz, die mit "0 Tage"
|
||||||
|
dasteht, liest sich wie eine tote.
|
||||||
|
|
||||||
|
**Es gibt genau diese eine Rechnung.** Der Lizenzserver hatte eine zweite,
|
||||||
|
abgerundete: dieselbe Lizenz zeigte in seiner Oberflaeche "21 Tage" und im
|
||||||
|
Abruf beim Kunden "22". Zwei Zahlen fuer dieselbe Frage sind schlimmer als
|
||||||
|
eine ungenaue -- sie kosten jemanden einen Nachmittag mit der Suche nach
|
||||||
|
einem Fehler, den es nicht gibt.
|
||||||
|
"""
|
||||||
|
moment = now or utcnow()
|
||||||
|
sekunden = int((expires_at - moment).total_seconds())
|
||||||
|
if sekunden <= 0:
|
||||||
|
return 0
|
||||||
|
return -(-sekunden // 86400)
|
||||||
|
|
||||||
|
|
||||||
def license_status(
|
def license_status(
|
||||||
document: Mapping[str, Any],
|
document: Mapping[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -278,7 +299,7 @@ def license_status(
|
|||||||
days_left = 0
|
days_left = 0
|
||||||
days_since_expiry = int((moment - expires_at).total_seconds() // 86400)
|
days_since_expiry = int((moment - expires_at).total_seconds() // 86400)
|
||||||
else:
|
else:
|
||||||
days_left = -(-int(delta.total_seconds()) // 86400)
|
days_left = tage_bis(expires_at, now=moment)
|
||||||
days_since_expiry = None
|
days_since_expiry = None
|
||||||
|
|
||||||
grace_active = bool(
|
grace_active = bool(
|
||||||
|
|||||||
Reference in new issue
Block a user