Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d9a46d8f |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.47"
|
version = "2.8.48"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -81,15 +81,61 @@ def overview():
|
|||||||
for zeile in Database.all(conn, "SELECT mac FROM devices WHERE deleted_at IS NULL")
|
for zeile in Database.all(conn, "SELECT mac FROM devices WHERE deleted_at IS NULL")
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template(
|
options = Database.all(
|
||||||
"tesm/dhcp.html",
|
|
||||||
subnets=Database.all(conn, "SELECT * FROM dhcp_subnets ORDER BY sort_order, id"),
|
|
||||||
options=Database.all(
|
|
||||||
conn,
|
conn,
|
||||||
"SELECT o.*, v.value, v.id AS value_id FROM dhcp_options o "
|
"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 "
|
"LEFT JOIN dhcp_option_values v ON v.option_id = o.id AND v.device_id IS NULL "
|
||||||
"ORDER BY o.code",
|
"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=options,
|
||||||
|
katalog=katalog,
|
||||||
|
gesendet=gesendet,
|
||||||
option_types=kea.OPTION_TYPES,
|
option_types=kea.OPTION_TYPES,
|
||||||
device_option_values=_device_option_values(conn),
|
device_option_values=_device_option_values(conn),
|
||||||
reservations=reservations,
|
reservations=reservations,
|
||||||
@@ -375,6 +421,17 @@ def add_custom_option():
|
|||||||
if not 1 <= code <= 254:
|
if not 1 <= code <= 254:
|
||||||
flash("Der Optionscode muss zwischen 1 und 254 liegen.", "danger")
|
flash("Der Optionscode muss zwischen 1 und 254 liegen.", "danger")
|
||||||
return redirect(url_for("dhcp.overview"))
|
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:
|
if not name or option_type not in kea.OPTION_TYPES:
|
||||||
flash("Name und Typ sind erforderlich.", "danger")
|
flash("Name und Typ sind erforderlich.", "danger")
|
||||||
return redirect(url_for("dhcp.overview"))
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|||||||
@@ -549,20 +549,13 @@ def render_config(
|
|||||||
# Server tatsaechlich sendet. Es wird nicht gespeichert, sondern aus dem
|
# Server tatsaechlich sendet. Es wird nicht gespeichert, sondern aus dem
|
||||||
# Katalog abgeleitet -- gespeichert waere es eine zweite Wahrheit.
|
# Katalog abgeleitet -- gespeichert waere es eine zweite Wahrheit.
|
||||||
belegte_codes = {int(row["code"]) for row in option_rows}
|
belegte_codes = {int(row["code"]) for row in option_rows}
|
||||||
gesendete_ids = set(global_values) | {
|
eintrag = verzeichnis(conn)
|
||||||
option_id for werte in device_values.values() for option_id in werte
|
if eintrag and KATALOG_CODE not in belegte_codes:
|
||||||
}
|
|
||||||
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:
|
|
||||||
custom_defs.append(
|
custom_defs.append(
|
||||||
{"name": KATALOG_NAME, "code": KATALOG_CODE, "type": "string", "space": "dhcp4"}
|
{"name": KATALOG_NAME, "code": KATALOG_CODE, "type": "string", "space": "dhcp4"}
|
||||||
)
|
)
|
||||||
global_option_data.append({"name": KATALOG_NAME, "data": verzeichnis})
|
global_option_data.append({"name": KATALOG_NAME, "data": eintrag})
|
||||||
elif verzeichnis:
|
elif eintrag:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"Code {KATALOG_CODE} ist von einer eigenen Option belegt | das Verzeichnis für "
|
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 "
|
"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]:
|
def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
|
||||||
"""Was ein Linux-Client braucht, damit die Optionen bei ihm ankommen.
|
"""Was ein Linux-Client braucht, damit die Optionen bei ihm ankommen.
|
||||||
|
|
||||||
|
|||||||
@@ -230,15 +230,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% 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">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('puzzle', size=17) }} Gesendete Optionen</div>
|
<div class="card__title">{{ icon('puzzle', size=17) }} Gesendete Optionen</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
{% if gesetzte %}
|
{% if gesendet %}
|
||||||
<strong>{{ gesetzte | length }}</strong> von {{ options | length }} Optionen tragen
|
<strong>{{ gesendet | length }}</strong> von {{ options | length }} Optionen tragen
|
||||||
einen Wert. Was leer ist, wird nicht gesendet.
|
einen Wert. Was leer ist, wird nicht gesendet.
|
||||||
{% else %}
|
{% else %}
|
||||||
Derzeit trägt keine Option einen Wert – es wird also nichts über die
|
Derzeit trägt keine Option einen Wert – es wird also nichts über die
|
||||||
@@ -248,7 +251,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if gesetzte %}
|
{% if gesendet %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
@@ -256,7 +259,7 @@
|
|||||||
<th style="width:42%">Wert</th><th class="actions"></th>
|
<th style="width:42%">Wert</th><th class="actions"></th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for option in gesetzte %}
|
{% for option in gesendet %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="numeric fs-sm">{{ option.code }}</td>
|
<td class="numeric fs-sm">{{ option.code }}</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
@@ -264,7 +267,19 @@
|
|||||||
<div class="fs-xs faint">{{ option.description }}</div>
|
<div class="fs-xs faint">{{ option.description }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-xs mono">{{ option.type }}</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>
|
<td class="actions">{{ optionsloeschen(option) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -284,7 +299,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('list', size=17) }} Optionskatalog</div>
|
<div class="card__title">{{ icon('list', size=17) }} Optionskatalog</div>
|
||||||
<div class="card__sub">
|
<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.
|
ihr Wert lässt sich leeren.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,7 +312,7 @@
|
|||||||
<th style="width:42%">Wert</th><th class="actions"></th>
|
<th style="width:42%">Wert</th><th class="actions"></th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for option in options %}
|
{% for option in katalog %}
|
||||||
<tr data-filter-text="{{ option.code }} {{ option.name }} {{ option.description }}">
|
<tr data-filter-text="{{ option.code }} {{ option.name }} {{ option.description }}">
|
||||||
<td class="numeric fs-sm">{{ option.code }}</td>
|
<td class="numeric fs-sm">{{ option.code }}</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
@@ -305,8 +320,14 @@
|
|||||||
<div class="fs-xs faint">{{ option.description }}</div>
|
<div class="fs-xs faint">{{ option.description }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-xs mono">{{ option.type }}</td>
|
<td class="fs-xs mono">{{ option.type }}</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>{{ optionswert(option) }}</td>
|
||||||
<td class="actions">{{ optionsloeschen(option) }}</td>
|
<td class="actions">{{ optionsloeschen(option) }}</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in new issue
Block a user