Version 2.8.48: belegter Code sichtbar, Geraetewerte zaehlen mit

This commit is contained in:
alientim committed 2026-09-17 17:17:37 +02:00
1 parent 5cc34f2a1f
commit 0a6e5190a2
4 files changed
+113 -28

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm"
version = "2.8.47"
version = "2.8.48"
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
requires-python = ">=3.11"
dependencies = [
+63 -6
View File
@@ -81,15 +81,61 @@ def overview():
for zeile in Database.all(conn, "SELECT mac FROM devices WHERE deleted_at IS NULL")
}
options = Database.all(
conn,
"SELECT o.*, v.value, v.id AS value_id FROM dhcp_options o "
"LEFT JOIN dhcp_option_values v ON v.option_id = o.id AND v.device_id IS NULL "
"ORDER BY o.code",
)
# Ein Wert an einem einzelnen Geraet ist ein gesendeter Wert. Die Uebersicht
# zaehlte nur die globalen -- und meldete "Nichts gesetzt", waehrend die
# Vorschau daneben die Option in der Reservierung auswies.
je_geraet: dict[int, list[dict[str, Any]]] = {}
for zeile in Database.all(
conn,
"SELECT o.code, d.name AS geraet, v.value FROM dhcp_option_values v "
"JOIN dhcp_options o ON o.id = v.option_id "
"JOIN devices d ON d.id = v.device_id "
"WHERE v.device_id IS NOT NULL AND d.deleted_at IS NULL "
"AND TRIM(COALESCE(v.value, '')) <> '' ORDER BY o.code, d.name",
):
je_geraet.setdefault(int(zeile["code"]), []).append(dict(zeile))
gesendet = []
for option in options:
geraete = je_geraet.get(int(option["code"]), [])
if str(option["value"] or "").strip() or geraete:
eintrag = dict(option)
eintrag["geraete"] = geraete
gesendet.append(eintrag)
# Der Server belegt einen Code selbst. Ihn aus dem Katalog herauszuhalten
# hiess: er steht in der erzeugten Datei, aber nirgends auf der Seite --
# und wer 224 vergeben will, erfaehrt es erst hinterher.
eigenes_verzeichnis = kea.verzeichnis(conn)
katalog = list(options)
if not any(int(zeile["code"]) == kea.KATALOG_CODE for zeile in katalog):
katalog.append(
{
"id": None,
"code": kea.KATALOG_CODE,
"name": kea.KATALOG_NAME,
"type": "string",
"description": "Vom Server belegt | nennt den Clients Name und Typ der "
"eigenen Optionen",
"value": eigenes_verzeichnis,
"is_standard": 1,
"reserviert": True,
}
)
katalog.sort(key=lambda zeile: int(zeile["code"]))
return render_template(
"tesm/dhcp.html",
subnets=Database.all(conn, "SELECT * FROM dhcp_subnets ORDER BY sort_order, id"),
options=Database.all(
conn,
"SELECT o.*, v.value, v.id AS value_id FROM dhcp_options o "
"LEFT JOIN dhcp_option_values v ON v.option_id = o.id AND v.device_id IS NULL "
"ORDER BY o.code",
),
options=options,
katalog=katalog,
gesendet=gesendet,
option_types=kea.OPTION_TYPES,
device_option_values=_device_option_values(conn),
reservations=reservations,
@@ -375,6 +421,17 @@ def add_custom_option():
if not 1 <= code <= 254:
flash("Der Optionscode muss zwischen 1 und 254 liegen.", "danger")
return redirect(url_for("dhcp.overview"))
if code == kea.KATALOG_CODE:
# Hier abweisen statt spaeter stillschweigend das Verzeichnis
# wegzulassen: sonst funktionierte die Benennung auf den Clients
# nicht mehr, und niemand wuesste warum.
flash(
f"Code {kea.KATALOG_CODE} ist belegt: darunter sendet der Server seinen Clients "
"die Namen und Typen der eigenen Optionen. Ohne dieses Verzeichnis heißt eine "
"neue Option auf dem Client nur noch nach ihrer Nummer.",
"danger",
)
return redirect(url_for("dhcp.overview"))
if not name or option_type not in kea.OPTION_TYPES:
flash("Name und Typ sind erforderlich.", "danger")
return redirect(url_for("dhcp.overview"))
+18 -11
View File
@@ -549,20 +549,13 @@ def render_config(
# Server tatsaechlich sendet. Es wird nicht gespeichert, sondern aus dem
# Katalog abgeleitet -- gespeichert waere es eine zweite Wahrheit.
belegte_codes = {int(row["code"]) for row in option_rows}
gesendete_ids = set(global_values) | {
option_id for werte in device_values.values() for option_id in werte
}
verzeichnis = " ".join(
f"{int(row['code'])}:{row['name']}:{row['type']}"
for row in option_rows
if not int(row["is_standard"] or 0) and int(row["id"]) in gesendete_ids
)
if verzeichnis and KATALOG_CODE not in belegte_codes:
eintrag = verzeichnis(conn)
if eintrag and KATALOG_CODE not in belegte_codes:
custom_defs.append(
{"name": KATALOG_NAME, "code": KATALOG_CODE, "type": "string", "space": "dhcp4"}
)
global_option_data.append({"name": KATALOG_NAME, "data": verzeichnis})
elif verzeichnis:
global_option_data.append({"name": KATALOG_NAME, "data": eintrag})
elif eintrag:
warnings.append(
f"Code {KATALOG_CODE} ist von einer eigenen Option belegt | das Verzeichnis für "
"die Clients wird nicht mitgesendet. Neue Optionen sind dort dann nur unter "
@@ -993,6 +986,20 @@ echo "ergaenzt: $ziel"
"""
def verzeichnis(conn: sqlite3.Connection) -> str:
"""Das Verzeichnis, das der Server seinen Clients mitsendet.
Eine Quelle fuer beides -- die Konfiguration und die Anzeige. Zweimal
berechnet waeren es zwei Wahrheiten, und die Seite wuerde irgendwann etwas
anderes zeigen als der Server sendet.
"""
return " ".join(
f"{int(zeile['code'])}:{zeile['name']}:{zeile['type']}"
for zeile in gesendete_optionen(conn)
if not int(zeile["is_standard"] or 0)
)
def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
"""Was ein Linux-Client braucht, damit die Optionen bei ihm ankommen.
+31 -10
View File
@@ -230,15 +230,18 @@
{% endif %}
{% endmacro %}
{% set gesetzte = options | selectattr('value') | list %}
{# "gesetzt" heisst nicht nur "global gesetzt": ein Wert an einem einzelnen
Geraet wird genauso gesendet. Die Liste kommt deshalb fertig vom Server --
in Jinja gerechnet zaehlte sie nur die globalen und meldete "Nichts
gesetzt", waehrend die Vorschau daneben die Option auswies. #}
<section class="card">
<div class="card__header">
<div class="flex-1">
<div class="card__title">{{ icon('puzzle', size=17) }} Gesendete Optionen</div>
<div class="card__sub">
{% if gesetzte %}
<strong>{{ gesetzte | length }}</strong> von {{ options | length }} Optionen tragen
{% if gesendet %}
<strong>{{ gesendet | length }}</strong> von {{ options | length }} Optionen tragen
einen Wert. Was leer ist, wird nicht gesendet.
{% else %}
Derzeit trägt keine Option einen Wert – es wird also nichts über die
@@ -248,7 +251,7 @@
</div>
</div>
{% if gesetzte %}
{% if gesendet %}
<div class="table-wrap">
<table class="table table--compact">
<thead><tr>
@@ -256,7 +259,7 @@
<th style="width:42%">Wert</th><th class="actions"></th>
</tr></thead>
<tbody>
{% for option in gesetzte %}
{% for option in gesendet %}
<tr>
<td class="numeric fs-sm">{{ option.code }}</td>
<td class="fs-sm">
@@ -264,7 +267,19 @@
<div class="fs-xs faint">{{ option.description }}</div>
</td>
<td class="fs-xs mono">{{ option.type }}</td>
<td>{{ optionswert(option) }}</td>
<td>
{{ optionswert(option) }}
{% if option.geraete %}
<div class="fs-xs faint mt-2">
{% for eintrag in option.geraete %}
<div>
nur für <strong>{{ eintrag.geraet }}</strong>:
<span class="mono">{{ eintrag.value }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</td>
<td class="actions">{{ optionsloeschen(option) }}</td>
</tr>
{% endfor %}
@@ -284,7 +299,7 @@
<div class="flex-1">
<div class="card__title">{{ icon('list', size=17) }} Optionskatalog</div>
<div class="card__sub">
Alle {{ options | length }} bekannten Optionen. Standardoptionen sind nicht löschbar,
Alle {{ katalog | length }} bekannten Optionen. Standardoptionen sind nicht löschbar,
ihr Wert lässt sich leeren.
</div>
</div>
@@ -297,7 +312,7 @@
<th style="width:42%">Wert</th><th class="actions"></th>
</tr></thead>
<tbody>
{% for option in options %}
{% for option in katalog %}
<tr data-filter-text="{{ option.code }} {{ option.name }} {{ option.description }}">
<td class="numeric fs-sm">{{ option.code }}</td>
<td class="fs-sm">
@@ -305,8 +320,14 @@
<div class="fs-xs faint">{{ option.description }}</div>
</td>
<td class="fs-xs mono">{{ option.type }}</td>
<td>{{ optionswert(option) }}</td>
<td class="actions">{{ optionsloeschen(option) }}</td>
{% if option.reserviert %}
{# Kein Eingabefeld: den Wert bestimmt der Katalog selbst. #}
<td class="fs-xs mono faint">{{ option.value or '(noch keine eigene Option)' }}</td>
<td class="actions">{{ pill('belegt', '') }}</td>
{% else %}
<td>{{ optionswert(option) }}</td>
<td class="actions">{{ optionsloeschen(option) }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>