Version 2.8.43: ein Netz, ein Eintrag -- und die Seite sagt, was wirklich laeuft
This commit is contained in:
1 parent
b1275b2c71
commit
f59ba42e55
5 files changed
+156
-14
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.8.42"
|
||||
version = "2.8.43"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -112,9 +112,24 @@ def overview():
|
||||
stil: kea.client_konfiguration(conn, stil) for stil, _, _ in kea.CLIENT_STILE
|
||||
},
|
||||
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
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -148,7 +163,13 @@ def add_subnet():
|
||||
start = (request.form.get("range_start") 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:
|
||||
flash(problem, "danger")
|
||||
return redirect(url_for("dhcp.overview"))
|
||||
@@ -183,7 +204,14 @@ def update_subnet(subnet_id: int):
|
||||
interface = (request.form.get("interface") or "").strip()
|
||||
start = (request.form.get("range_start") 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:
|
||||
flash(problem, "danger")
|
||||
return redirect(url_for("dhcp.overview"))
|
||||
@@ -608,7 +636,13 @@ def _write_and_redirect():
|
||||
for warning in result.get("warnings", []):
|
||||
flash(warning, "warning")
|
||||
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"]:
|
||||
flash(result["message"], "info")
|
||||
return redirect(url_for("dhcp.overview"))
|
||||
|
||||
@@ -186,9 +186,20 @@ def matching_network(
|
||||
|
||||
|
||||
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:
|
||||
"""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:
|
||||
return "Es ist kein Interface ausgewählt."
|
||||
try:
|
||||
@@ -203,9 +214,41 @@ def validate_subnet(
|
||||
f"Der Bereich {range_start}-{range_end} passt in kein Netz von {interface}. "
|
||||
"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 ""
|
||||
|
||||
|
||||
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
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -393,8 +436,16 @@ def render_config(
|
||||
return entries
|
||||
|
||||
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()
|
||||
for index, subnet in enumerate(subnets, start=1):
|
||||
for subnet in subnets:
|
||||
interface = str(subnet["interface"])
|
||||
match = matching_network(
|
||||
networks, interface, str(subnet["range_start"]), str(subnet["range_end"])
|
||||
@@ -433,15 +484,40 @@ def render_config(
|
||||
entry["option-data"] = option_data(overrides)
|
||||
subnet_reservations.append(entry)
|
||||
|
||||
subnet_entries.append(
|
||||
{
|
||||
"id": index,
|
||||
"subnet": str(network),
|
||||
"pools": [{"pool": f"{subnet['range_start']} - {subnet['range_end']}"}],
|
||||
schluessel = str(network)
|
||||
eintrag = nach_netz.get(schluessel)
|
||||
if eintrag is None:
|
||||
eintrag = {
|
||||
# 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 {}),
|
||||
"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:
|
||||
warnings.append("Es ist kein gültiges Subnetz konfiguriert | Kea würde nicht starten.")
|
||||
|
||||
@@ -7,6 +7,29 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 – 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 %}
|
||||
{% if not sysops_available %}
|
||||
{{ alert('Systemhelfer nicht verfügbar -- Konfiguration und Dienstzustand können hier nur
|
||||
@@ -21,8 +44,13 @@
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat__label">Programm</span>
|
||||
<span class="stat__value fs-lg">{{ service.get('binary', '?') }}</span>
|
||||
<span class="stat__hint">kea-dhcp4</span>
|
||||
{# Die Fassung statt nur "vorhanden": zwischen Kea 2.4 (Ubuntu 24.04) und
|
||||
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') %} · {{ service.get('binary', '?') }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat__label">Reservierungen</span>
|
||||
|
||||
@@ -100,6 +100,10 @@ check_readable_path() {
|
||||
case "$1" in
|
||||
/var/log/kea/kea-dhcp4.log) ;;
|
||||
/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)
|
||||
check_app_key "$(basename "$1" .yaml | sed 's/^90-//')" ;;
|
||||
/etc/nginx/sites-available/*)
|
||||
|
||||
Reference in new issue
Block a user