From 5cc34f2a1fc879789c1df75bfcf968d1506c9482 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Thu, 17 Sep 2026 16:22:19 +0200 Subject: [PATCH] Version 2.8.47: Verzeichnis der eigenen Optionen --- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/blueprints/dhcp.py | 10 ++ apps/tesm/src/tesm/services/kea.py | 130 ++++++++++++++++++-- apps/tesm/src/tesm/templates/tesm/dhcp.html | 5 +- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 68aec39..675ef07 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.46" +version = "2.8.47" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/dhcp.py b/apps/tesm/src/tesm/blueprints/dhcp.py index 6691532..6ce743d 100644 --- a/apps/tesm/src/tesm/blueprints/dhcp.py +++ b/apps/tesm/src/tesm/blueprints/dhcp.py @@ -389,6 +389,16 @@ def add_custom_option(): ) _audit("dhcp.option_created", name, severity="notice") flash("Eigene Option angelegt.", "success") + if code not in kea.PRIVATBEREICH: + # Kein Fehler -- andere Clients nehmen auch andere Codes. Aber ein + # Linux-Client mit systemd-networkd hebt eine unbekannte Option nur + # dann auf, wenn ihr Code im vorgesehenen Bereich liegt. Wer das + # nicht weiss, sucht den Fehler danach beim Server. + flash( + f"Code {code} liegt außerhalb von 224–254. Clients mit systemd-networkd " + "verwerfen Optionen außerhalb dieses Bereichs, auch wenn sie sie anfordern.", + "warning", + ) return redirect(url_for("dhcp.overview")) diff --git a/apps/tesm/src/tesm/services/kea.py b/apps/tesm/src/tesm/services/kea.py index e355436..2b7746e 100644 --- a/apps/tesm/src/tesm/services/kea.py +++ b/apps/tesm/src/tesm/services/kea.py @@ -545,6 +545,30 @@ def render_config( if not int(row["is_standard"] or 0) ] + # Das Verzeichnis: Code, Name und Typ der eigenen Optionen, die dieser + # 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: + custom_defs.append( + {"name": KATALOG_NAME, "code": KATALOG_CODE, "type": "string", "space": "dhcp4"} + ) + global_option_data.append({"name": KATALOG_NAME, "data": verzeichnis}) + elif verzeichnis: + 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 " + "ihrer Nummer zu sehen, bis die Konfiguration erneut verteilt wird." + ) + config = { "Dhcp4": { "interfaces-config": {"interfaces": sorted(interfaces)}, @@ -729,6 +753,26 @@ def read_leases(sysops: SysOps, *, path: str = LEASE_FILE) -> list[Lease]: return sorted(latest.values(), key=lambda entry: tuple(int(p) for p in entry.address.split(".") if p.isdigit())) +#: Der Bereich, den DHCP fuer eigene Optionen vorsieht (RFC 2132: "site +#: specific"). **Nur** hier hinein gehoeren eigene Codes, und zwar nicht aus +#: Ordnungsliebe: systemd-networkd hebt eine unbekannte Option ausschliesslich +#: dann auf, wenn ihr Code in diesem Bereich liegt. Alles darunter wirft es +#: weg, ohne dass irgendwo etwas danebengeht. +PRIVATBEREICH = range(224, 255) + +#: Code und Name des Verzeichnisses, das der Server ueber sich selbst mitsendet. +#: +#: **Warum es das gibt.** Auf der Leitung reisen nur Code, Laenge und Bytes -- +#: kein Name, kein Typ. Ein Client kann darum nicht wissen, dass 225 "url" +#: heisst und Text enthaelt. Bisher bekam er die Zuordnung beim Verteilen +#: mit; kam spaeter eine Option dazu, musste man erneut verteilen. Also sendet +#: der Server sein Verzeichnis einfach mit: "225:url:string 226:monitor:...". +#: Der Uebersetzer auf dem Client liest es bei jeder Lease neu -- eine neue +#: Option ist damit sofort unter ihrem Namen lesbar, ohne den Client +#: anzufassen. +KATALOG_CODE = 224 +KATALOG_NAME = "tesm-katalog" + #: Optionen, die jeder DHCP-Client von sich aus erfragt. Sie in die #: Client-Konfiguration zu schreiben waere Ballast -- und wer Ballast liest, #: liest irgendwann gar nicht mehr. @@ -838,22 +882,71 @@ cat /run/tesm-dhcp-options.env 2>/dev/null || true #: Bytes. Dass 225 "url" heisst und eine Zeichenkette ist, steht allein im #: Katalog dieses Servers -- systemd kann es nicht wissen und legt deshalb #: Hex unter der Nummer ab. Diese Zuordnung reist mit. -UEBERSETZER = """#!/bin/sh +UEBERSETZER = r"""#!/bin/sh # Von TESM erzeugt. Schreibt die eigenen DHCP-Optionen im Klartext nach # /run/tesm-dhcp-options.env -- eine Zeile "name=wert" je Option. +# +# Uebersetzt wird **alles**, was in der Lease steht, nicht eine feste Liste: +# Namen und Typen kommen aus dem Verzeichnis, das der Server als Option +# @KATALOG@ mitsendet. Eine neue Option ist damit sofort unter ihrem Namen +# lesbar, ohne dass hier etwas geaendert werden muesste. Fehlt das +# Verzeichnis, gilt die Zuordnung vom Tag des Verteilens; fehlt auch die, +# heisst die Option nach ihrer Nummer. set -e ziel=/run/tesm-dhcp-options.env +ersatz="@PAARE@" + schnittstelle=$(ip -o -4 route show default 2>/dev/null | awk '{print $5; exit}') [ -n "$schnittstelle" ] || exit 0 lease=/run/systemd/netif/leases/$(cat /sys/class/net/"$schnittstelle"/ifindex 2>/dev/null) [ -f "$lease" ] || exit 0 + +text() { /usr/bin/printf "$(echo "$1" | sed 's/../@BS@x&/g')"; } + +entziffere() { + # $1 = Hexfolge, $2 = Typ. Ohne den Typ waere jede Adresse Buchstabensalat: + # eine IPv4-Adresse reist als vier rohe Bytes, nicht als "192.168.0.1". + case "$2" in + ipv4-address) + set -- $(echo "$1" | sed 's/../& /g') + [ $# -ge 4 ] || return 0 + printf '%d.%d.%d.%d' "0x$1" "0x$2" "0x$3" "0x$4" + ;; + uint8|uint16|uint32) + printf '%d' "0x$1" + ;; + boolean) + case "$1" in 00) printf 'nein' ;; *) printf 'ja' ;; esac + ;; + *) + text "$1" + ;; + esac +} + +nachschlagen() { + # Erst im mitgesendeten Verzeichnis, dann in der Zuordnung vom Verteilen. + for eintrag in $verzeichnis $ersatz; do + case "$eintrag" in + "$1":*) + rest=${eintrag#*:} + echo "${rest%%:*} ${rest#*:}" + return 0 + ;; + esac + done + echo "option_$1 string" +} + +verzeichnis=$(text "$(sed -n 's/^OPTION_@KATALOG@=//p' "$lease" | head -n1)") + zwischen=$(mktemp) -for paar in @PAARE@; do - code=${paar%%:*} - name=${paar#*:} - roh=$(sed -n "s/^OPTION_$code=//p" "$lease" | head -n1) - [ -n "$roh" ] || continue - wert=$(/usr/bin/printf "$(echo "$roh" | sed 's/../@BS@x&/g')") +sed -n 's/^OPTION_\([0-9]*\)=\(.*\)$/\1 \2/p' "$lease" | while read -r code hex; do + [ "$code" = "@KATALOG@" ] && continue + angabe=$(nachschlagen "$code") + name=${angabe%% *} + typ=${angabe#* } + wert=$(entziffere "$hex" "$typ") echo "$name=$wert" >> "$zwischen" done mv -f "$zwischen" "$ziel" @@ -935,10 +1028,19 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]: "#", "# Die Werte stehen danach in /run/systemd/netif/leases/", "# als OPTION_= -- hexkodiert, weil systemd fremde Optionen nicht", - "# deutet.", + "# deutet. Den Klartext schreibt der mitgelieferte Uebersetzer nach", + "# /run/tesm-dhcp-options.env.", + "#", + "# Angefordert wird der ganze Bereich 224-254, damit eine spaeter", + "# hinzugefuegte Option ohne Zutun ankommt. Eigene Optionen ausserhalb", + "# dieses Bereichs hebt systemd-networkd nicht auf.", "[DHCPv4]", ] - zeilen.append("RequestOptions=" + " ".join(str(code) for code in codes)) + # Angefordert wird der **ganze** eigene Bereich, nicht nur das, was + # heute gesendet wird: sonst muesste man nach jeder neuen Option zu + # jedem Client. Ein Client bekommt trotzdem nur, was der Server hat. + angefragt = sorted(set(PRIVATBEREICH) | set(codes)) + zeilen.append("RequestOptions=" + " ".join(str(code) for code in angefragt)) nachher = "networkctl reload && networkctl reconfigure " elif stil == "dhcpcd": zeilen = [ @@ -1045,8 +1147,14 @@ def _uebernahme_skript(stil: str, inhalt: str, benannt: list[dict[str, Any]] | N benannt = benannt or [] if stil == "networkd": - paare = " ".join(f"{eintrag['code']}:{eintrag['name']}" for eintrag in benannt) - uebersetzer = UEBERSETZER.replace("@PAARE@", paare).replace("@BS@", "\\\\") # sed gibt aus zwei Zeichen eines aus + paare = " ".join( + f"{eintrag['code']}:{eintrag['name']}:{eintrag['type']}" for eintrag in benannt + ) + uebersetzer = ( + UEBERSETZER.replace("@PAARE@", paare) + .replace("@KATALOG@", str(KATALOG_CODE)) + .replace("@BS@", "\\\\") # sed gibt aus zwei Zeichen eines aus + ) return ( NETWORKD_SKRIPT.replace("@FRACHT@", fracht) .replace("@UEBERSETZER@", _b64(uebersetzer)) diff --git a/apps/tesm/src/tesm/templates/tesm/dhcp.html b/apps/tesm/src/tesm/templates/tesm/dhcp.html index 809ddac..7f6894c 100644 --- a/apps/tesm/src/tesm/templates/tesm/dhcp.html +++ b/apps/tesm/src/tesm/templates/tesm/dhcp.html @@ -321,6 +321,8 @@
{{ icon('plus', size=17) }} Eigene Option anlegen
Für alles jenseits des Katalogs – etwa eine URL, die der Client beim Start öffnet. + Code 224–254: nur diesen Bereich sieht DHCP für eigene Optionen vor, + und nur daraus hebt ein Linux-Client mit systemd-networkd sie überhaupt auf.
@@ -328,7 +330,8 @@
- {{ field('code', 'Code', type='number', required=True, placeholder='225') }} + {{ field('code', 'Code', type='number', required=True, placeholder='226', + hint_text='224 bis 254') }} {{ field('name', 'Name', required=True, placeholder='meine-option') }}