Compare commits

...
1 Commits
5 changed files with 156 additions and 14 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm" name = "tesm"
version = "2.8.42" version = "2.8.43"
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 = [
+37 -3
View File
@@ -112,9 +112,24 @@ def overview():
stil: kea.client_konfiguration(conn, stil) for stil, _, _ in kea.CLIENT_STILE stil: kea.client_konfiguration(conn, stil) for stil, _, _ in kea.CLIENT_STILE
}, },
sysops_available=bool(sysops and sysops.available), sysops_available=bool(sysops and sysops.available),
# "gespeichert" heisst nicht "wirksam": lehnt Kea die Datei ab, bleibt
# der Dienst auf dem alten Stand, waehrend die Seite den neuen Wert
# zeigt. Deshalb wird hier verglichen, was auf der Platte liegt.
konfig_stand=_konfig_stand(config),
) )
def _konfig_stand(gewuenscht: dict[str, Any]) -> str:
"""``aktuell``, ``abweichend`` oder ``unbekannt``."""
sysops = core().sysops
if sysops is None or not sysops.available:
return "unbekannt"
vorhanden = kea.laufende_konfiguration(sysops)
if vorhanden is None:
return "unbekannt"
return "aktuell" if vorhanden == gewuenscht else "abweichend"
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Einstellungen und Subnetze # Einstellungen und Subnetze
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@@ -148,7 +163,13 @@ def add_subnet():
start = (request.form.get("range_start") or "").strip() start = (request.form.get("range_start") or "").strip()
end = (request.form.get("range_end") or "").strip() end = (request.form.get("range_end") or "").strip()
problem = kea.validate_subnet(networks, interface, start, end) problem = kea.validate_subnet(
networks,
interface,
start,
end,
vorhandene=Database.all(conn, "SELECT * FROM dhcp_subnets"),
)
if problem: if problem:
flash(problem, "danger") flash(problem, "danger")
return redirect(url_for("dhcp.overview")) return redirect(url_for("dhcp.overview"))
@@ -183,7 +204,14 @@ def update_subnet(subnet_id: int):
interface = (request.form.get("interface") or "").strip() interface = (request.form.get("interface") or "").strip()
start = (request.form.get("range_start") or "").strip() start = (request.form.get("range_start") or "").strip()
end = (request.form.get("range_end") or "").strip() end = (request.form.get("range_end") or "").strip()
problem = kea.validate_subnet(networks, interface, start, end) problem = kea.validate_subnet(
networks,
interface,
start,
end,
vorhandene=Database.all(conn, "SELECT * FROM dhcp_subnets"),
eigene_id=subnet_id,
)
if problem: if problem:
flash(problem, "danger") flash(problem, "danger")
return redirect(url_for("dhcp.overview")) return redirect(url_for("dhcp.overview"))
@@ -608,7 +636,13 @@ def _write_and_redirect():
for warning in result.get("warnings", []): for warning in result.get("warnings", []):
flash(warning, "warning") flash(warning, "warning")
if not result["ok"] and result.get("message"): if not result["ok"] and result.get("message"):
flash(result["message"], "warning") # Rot, nicht orange: die Aenderung steht in der Datenbank und auf der
# Seite, wirkt aber nicht. Das ist der Zustand, in dem man stundenlang
# sucht, warum ein Client die Option nicht bekommt.
flash(
result["message"] + " Die Änderung ist gespeichert, aber noch nicht wirksam.",
"danger",
)
elif result["ok"]: elif result["ok"]:
flash(result["message"], "info") flash(result["message"], "info")
return redirect(url_for("dhcp.overview")) return redirect(url_for("dhcp.overview"))
+84 -8
View File
@@ -186,9 +186,20 @@ def matching_network(
def validate_subnet( def validate_subnet(
networks: Iterable[InterfaceNetwork], interface: str, range_start: str, range_end: str networks: Iterable[InterfaceNetwork],
interface: str,
range_start: str,
range_end: str,
*,
vorhandene: Iterable[Any] = (),
eigene_id: int | None = None,
) -> str: ) -> str:
"""Rueckgabe: leerer String bei Erfolg, sonst die Fehlermeldung.""" """Rueckgabe: leerer String bei Erfolg, sonst die Fehlermeldung.
``vorhandene`` sind die schon eingetragenen Bereiche. Ueberschneidet sich
der neue mit einem davon, entsteht eine Konfiguration, die Kea als Ganzes
ablehnt -- besser hier sagen als spaeter beim Schreiben.
"""
if not interface: if not interface:
return "Es ist kein Interface ausgewählt." return "Es ist kein Interface ausgewählt."
try: try:
@@ -203,9 +214,41 @@ def validate_subnet(
f"Der Bereich {range_start}-{range_end} passt in kein Netz von {interface}. " f"Der Bereich {range_start}-{range_end} passt in kein Netz von {interface}. "
"Subnetz und Maske werden vom laufenden System gelesen." "Subnetz und Maske werden vom laufenden System gelesen."
) )
for zeile in vorhandene:
if eigene_id is not None and int(zeile["id"]) == eigene_id:
continue
if str(zeile["interface"]) != interface:
continue
try:
anfang = int(ipaddress.ip_address(str(zeile["range_start"])))
ende = int(ipaddress.ip_address(str(zeile["range_end"])))
except ValueError:
continue
if int(start) <= ende and int(end) >= anfang:
return (
f"Der Bereich überschneidet sich mit {zeile['range_start']}-"
f"{zeile['range_end']} auf {interface}. Erweitern Sie den vorhandenen "
"Bereich, statt einen zweiten anzulegen."
)
return "" return ""
def laufende_konfiguration(sysops: SysOps) -> dict[str, Any] | None:
"""Die Konfiguration, die tatsaechlich auf der Platte liegt.
``None`` heisst: nicht lesbar (kein Helfer, Datei fehlt, kein JSON) -- das
ist etwas anderes als "weicht ab" und wird auch anders angezeigt.
"""
result = sysops.read_privileged_file(DEFAULT_CONFIG_PATH)
if not result.ok or not (result.stdout or "").strip():
return None
try:
geladen = json.loads(result.stdout)
except (json.JSONDecodeError, ValueError):
return None
return geladen if isinstance(geladen, dict) else None
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Reservierungen # Reservierungen
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@@ -393,8 +436,16 @@ def render_config(
return entries return entries
subnet_entries: list[dict[str, Any]] = [] subnet_entries: list[dict[str, Any]] = []
# Ein Netz, ein Eintrag. Kea lehnt die **ganze** Konfiguration ab, sobald
# derselbe Praefix zweimal darin steht ("subnet with the prefix of
# '192.168.82.0/24' already exists") -- und dann laeuft der Dienst
# unbemerkt mit dem alten Stand weiter. Zwei Zeilen im selben Netz sind
# aber nichts Unsinniges: es sind zwei Bereiche. Sie werden deshalb zu
# einem Subnetz mit mehreren Pools zusammengefasst.
nach_netz: dict[str, dict[str, Any]] = {}
belegte_bereiche: dict[str, list[tuple[int, int]]] = {}
interfaces: set[str] = set() interfaces: set[str] = set()
for index, subnet in enumerate(subnets, start=1): for subnet in subnets:
interface = str(subnet["interface"]) interface = str(subnet["interface"])
match = matching_network( match = matching_network(
networks, interface, str(subnet["range_start"]), str(subnet["range_end"]) networks, interface, str(subnet["range_start"]), str(subnet["range_end"])
@@ -433,15 +484,40 @@ def render_config(
entry["option-data"] = option_data(overrides) entry["option-data"] = option_data(overrides)
subnet_reservations.append(entry) subnet_reservations.append(entry)
subnet_entries.append( schluessel = str(network)
{ eintrag = nach_netz.get(schluessel)
"id": index, if eintrag is None:
"subnet": str(network), eintrag = {
"pools": [{"pool": f"{subnet['range_start']} - {subnet['range_end']}"}], # Die Nummer zaehlt die Eintraege, nicht die Zeilen der
# Tabelle -- sonst haette ein uebersprungenes Subnetz eine
# Luecke hinterlassen, und Kea verlangt eindeutige IDs.
"id": len(subnet_entries) + 1,
"subnet": schluessel,
"pools": [],
**({"option-data": subnet_options} if subnet_options else {}), **({"option-data": subnet_options} if subnet_options else {}),
"reservations": subnet_reservations, "reservations": subnet_reservations,
} }
nach_netz[schluessel] = eintrag
belegte_bereiche[schluessel] = []
subnet_entries.append(eintrag)
elif subnet_options and subnet_options != eintrag.get("option-data", []):
warnings.append(
f"Für {schluessel} sind mehrere Bereiche mit unterschiedlichem Gateway oder "
"DNS eingetragen. Ein Netz kann nur eines haben | es gilt der erste Eintrag."
)
# Auch zwei Pools, die sich ueberschneiden, bringen Kea zu Fall.
von, bis = int(ipaddress.ip_address(str(subnet["range_start"]))), int(
ipaddress.ip_address(str(subnet["range_end"]))
) )
if any(von <= ende and bis >= anfang for anfang, ende in belegte_bereiche[schluessel]):
warnings.append(
f"Der Bereich {subnet['range_start']}-{subnet['range_end']} überschneidet sich "
"mit einem bereits eingetragenen und wurde übersprungen."
)
continue
belegte_bereiche[schluessel].append((von, bis))
eintrag["pools"].append({"pool": f"{subnet['range_start']} - {subnet['range_end']}"})
if not subnet_entries: if not subnet_entries:
warnings.append("Es ist kein gültiges Subnetz konfiguriert | Kea würde nicht starten.") warnings.append("Es ist kein gültiges Subnetz konfiguriert | Kea würde nicht starten.")
+30 -2
View File
@@ -7,6 +7,29 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{# Der wichtigste Satz der Seite, wenn er zutrifft: was hier steht, ist nicht,
womit der Dienst arbeitet. Lehnt Kea eine Datei ab, bleibt der alte Stand
aktiv -- die Oberflaeche zeigte bisher trotzdem den neuen Wert, und man
sucht den Fehler beim Client. #}
{% if konfig_stand == 'abweichend' %}
<div class="alert alert--danger">
{{ icon('alert', size=18) }}
<div class="alert__body flex-1">
<div class="alert__title">Der Dienst arbeitet nicht mit dieser Konfiguration</div>
Auf der Platte liegt ein anderer Stand als der hier gezeigte &ndash; entweder wurde
die Datei zuletzt abgelehnt oder seither nichts geschrieben. Änderungen an Optionen
und Reservierungen sind gespeichert, aber <strong>nicht wirksam</strong>.
{% if current_user.has('dhcp_config.execute') %}
<form method="post" action="{{ url_for('dhcp.write_config') }}" class="mt-2" data-no-guard>
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
<button class="btn btn--sm btn--primary" type="submit" data-busy-label="Schreibt ...">
Jetzt schreiben und Fehler anzeigen
</button>
</form>
{% endif %}
</div>
</div>
{% endif %}
{% for warning in warnings %}{{ alert(warning, 'warning') }}{% endfor %} {% for warning in warnings %}{{ alert(warning, 'warning') }}{% endfor %}
{% if not sysops_available %} {% if not sysops_available %}
{{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur {{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur
@@ -21,8 +44,13 @@
</div> </div>
<div class="stat"> <div class="stat">
<span class="stat__label">Programm</span> <span class="stat__label">Programm</span>
<span class="stat__value fs-lg">{{ service.get('binary', '?') }}</span> {# Die Fassung statt nur "vorhanden": zwischen Kea 2.4 (Ubuntu 24.04) und
<span class="stat__hint">kea-dhcp4</span> 3.0 (26.04) liegen Aenderungen, und wer eine Konfiguration beurteilt,
will wissen, wogegen. #}
<span class="stat__value fs-lg">{{ service.get('version') or service.get('binary', '?') }}</span>
<span class="stat__hint">
kea-dhcp4{% if service.get('version') %} &middot; {{ service.get('binary', '?') }}{% endif %}
</span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="stat__label">Reservierungen</span> <span class="stat__label">Reservierungen</span>
+4
View File
@@ -100,6 +100,10 @@ check_readable_path() {
case "$1" in case "$1" in
/var/log/kea/kea-dhcp4.log) ;; /var/log/kea/kea-dhcp4.log) ;;
/var/lib/kea/kea-leases4.csv) ;; /var/lib/kea/kea-leases4.csv) ;;
# Die eigene Kea-Konfiguration: 0640 root:_kea, also fuer den
# Dienstbenutzer nicht lesbar. Die Anwendung braucht sie, um zu sehen, ob
# der Dienst ueberhaupt mit dem laeuft, was sie geschrieben hat.
/etc/kea/kea-dhcp4.conf) ;;
/etc/netplan/90-*.yaml) /etc/netplan/90-*.yaml)
check_app_key "$(basename "$1" .yaml | sed 's/^90-//')" ;; check_app_key "$(basename "$1" .yaml | sed 's/^90-//')" ;;
/etc/nginx/sites-available/*) /etc/nginx/sites-available/*)