Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65a7810cf2 | ||
|
|
a6d9a46d8f | ||
|
|
5cc34f2a1f | ||
|
|
e284d70855 |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.45"
|
version = "2.8.49"
|
||||||
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,65 @@ 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",
|
||||||
|
# Die Regel gehoert dorthin, wo man sie sucht: das Verzeichnis
|
||||||
|
# nennt die Optionen, die **gesendet** werden. Eine angelegte
|
||||||
|
# Option ohne Wert steht nicht darin -- sie geht ja auch nicht
|
||||||
|
# hinaus, und ein Client braucht ihren Namen nicht.
|
||||||
|
"description": "Vom Server belegt | nennt den Clients Name und Typ der "
|
||||||
|
"Optionen, die gerade gesendet werden",
|
||||||
|
"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 +425,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"))
|
||||||
@@ -389,6 +450,16 @@ def add_custom_option():
|
|||||||
)
|
)
|
||||||
_audit("dhcp.option_created", name, severity="notice")
|
_audit("dhcp.option_created", name, severity="notice")
|
||||||
flash("Eigene Option angelegt.", "success")
|
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"))
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -545,6 +545,23 @@ def render_config(
|
|||||||
if not int(row["is_standard"] or 0)
|
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}
|
||||||
|
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": 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 "
|
||||||
|
"ihrer Nummer zu sehen, bis die Konfiguration erneut verteilt wird."
|
||||||
|
)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"Dhcp4": {
|
"Dhcp4": {
|
||||||
"interfaces-config": {"interfaces": sorted(interfaces)},
|
"interfaces-config": {"interfaces": sorted(interfaces)},
|
||||||
@@ -729,6 +746,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()))
|
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
|
#: Optionen, die jeder DHCP-Client von sich aus erfragt. Sie in die
|
||||||
#: Client-Konfiguration zu schreiben waere Ballast -- und wer Ballast liest,
|
#: Client-Konfiguration zu schreiben waere Ballast -- und wer Ballast liest,
|
||||||
#: liest irgendwann gar nicht mehr.
|
#: liest irgendwann gar nicht mehr.
|
||||||
@@ -807,9 +844,131 @@ mkdir -p "$(dirname "$ziel")"
|
|||||||
printf %s '@FRACHT@' | base64 -d > "$ziel"
|
printf %s '@FRACHT@' | base64 -d > "$ziel"
|
||||||
chmod 0644 "$ziel"
|
chmod 0644 "$ziel"
|
||||||
echo "geschrieben: $ziel"
|
echo "geschrieben: $ziel"
|
||||||
|
|
||||||
|
# Der Uebersetzer: systemd legt fremde Optionen roh und unter ihrer Nummer ab
|
||||||
|
# ("OPTION_225=687474..."), weil im DHCP-Paket weder Name noch Typ steht.
|
||||||
|
# Beides weiss nur dieser Server -- also schreibt er die Zuordnung mit.
|
||||||
|
mkdir -p /usr/local/lib/tesm
|
||||||
|
printf %s '@UEBERSETZER@' | base64 -d > /usr/local/lib/tesm/dhcp-options
|
||||||
|
chmod 0755 /usr/local/lib/tesm/dhcp-options
|
||||||
|
printf %s '@DIENST@' | base64 -d > /etc/systemd/system/tesm-dhcp-options.service
|
||||||
|
printf %s '@PFADEINHEIT@' | base64 -d > /etc/systemd/system/tesm-dhcp-options.path
|
||||||
|
chmod 0644 /etc/systemd/system/tesm-dhcp-options.service /etc/systemd/system/tesm-dhcp-options.path
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now tesm-dhcp-options.path >/dev/null 2>&1 || true
|
||||||
|
systemctl enable tesm-dhcp-options.service >/dev/null 2>&1 || true
|
||||||
|
|
||||||
networkctl reload
|
networkctl reload
|
||||||
networkctl reconfigure "$schnittstelle"
|
networkctl reconfigure "$schnittstelle"
|
||||||
|
# Der Pfadwaechter loest bei der naechsten Lease aus; einmal von Hand, damit
|
||||||
|
# die Datei sofort dasteht.
|
||||||
|
sleep 2
|
||||||
|
systemctl start tesm-dhcp-options.service || true
|
||||||
echo "Lease: /run/systemd/netif/leases/$(cat /sys/class/net/"$schnittstelle"/ifindex)"
|
echo "Lease: /run/systemd/netif/leases/$(cat /sys/class/net/"$schnittstelle"/ifindex)"
|
||||||
|
echo "Klartext: /run/tesm-dhcp-options.env"
|
||||||
|
cat /run/tesm-dhcp-options.env 2>/dev/null || true
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Uebersetzt die rohen Optionen der Lease in "name=wert".
|
||||||
|
#:
|
||||||
|
#: Warum es das braucht: in einem DHCP-Paket stehen nur Nummer, Laenge und
|
||||||
|
#: 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 = 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)
|
||||||
|
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"
|
||||||
|
chmod 0644 "$ziel"
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Einmal beim Start und nach jeder Lease.
|
||||||
|
DIENST_EINHEIT = """[Unit]
|
||||||
|
Description=TESM -- DHCP-Optionen im Klartext
|
||||||
|
After=systemd-networkd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/lib/tesm/dhcp-options
|
||||||
|
RemainAfterExit=no
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"""
|
||||||
|
|
||||||
|
PFAD_EINHEIT = """[Unit]
|
||||||
|
Description=TESM -- wacht ueber die DHCP-Lease
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathModified=/run/systemd/netif/leases
|
||||||
|
Unit=tesm-dhcp-options.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: Ersetzt den eigenen Block in einer Datei, die dem Client gehoert.
|
#: Ersetzt den eigenen Block in einer Datei, die dem Client gehoert.
|
||||||
@@ -827,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.
|
||||||
|
|
||||||
@@ -862,10 +1035,19 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
|
|||||||
"#",
|
"#",
|
||||||
"# Die Werte stehen danach in /run/systemd/netif/leases/<ifindex>",
|
"# Die Werte stehen danach in /run/systemd/netif/leases/<ifindex>",
|
||||||
"# als OPTION_<code>= -- hexkodiert, weil systemd fremde Optionen nicht",
|
"# als OPTION_<code>= -- 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]",
|
"[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 <Schnittstelle>"
|
nachher = "networkctl reload && networkctl reconfigure <Schnittstelle>"
|
||||||
elif stil == "dhcpcd":
|
elif stil == "dhcpcd":
|
||||||
zeilen = [
|
zeilen = [
|
||||||
@@ -902,35 +1084,55 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
|
|||||||
"pfad": pfad,
|
"pfad": pfad,
|
||||||
"inhalt": inhalt,
|
"inhalt": inhalt,
|
||||||
"nachher": nachher,
|
"nachher": nachher,
|
||||||
"skript": _uebernahme_skript(stil, inhalt),
|
"skript": _uebernahme_skript(stil, inhalt, optionen),
|
||||||
"auslesen": _auslesen(stil, codes),
|
"auslesen": _auslesen(stil, optionen),
|
||||||
"optionen": optionen,
|
"optionen": optionen,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _auslesen(stil: str, codes: list[int]) -> str:
|
def _auslesen(stil: str, benannt: list[dict[str, Any]]) -> dict[str, str]:
|
||||||
"""Wie der Wert auf dem Client wieder herauskommt.
|
"""Wie der Wert auf dem Client wieder herauskommt.
|
||||||
|
|
||||||
Gehoert dazu, weil "angekommen" sonst niemand pruefen kann -- und weil ein
|
Gehoert dazu, weil "angekommen" sonst niemand pruefen kann -- und weil ein
|
||||||
RMM-Agent genau diese Zeile braucht. Bei systemd-networkd steht der Wert
|
RMM-Agent genau diese Zeile braucht.
|
||||||
**hexkodiert** in der Lease: systemd kennt fremde Optionen nicht und legt
|
|
||||||
sie roh ab. Ohne das Umrechnen liest man 687474703a... und haelt es fuer
|
Zwei Wege bei systemd-networkd, und das hat einen Grund: in der Lease
|
||||||
Unsinn.
|
steht der Wert **hexkodiert unter seiner Nummer**. Im DHCP-Paket reisen
|
||||||
|
nur Code, Laenge und Bytes; dass 225 "url" heisst und Text enthaelt, weiss
|
||||||
|
allein der Katalog dieses Servers. Deshalb legt der SSH-Push einen kleinen
|
||||||
|
Uebersetzer mit, der nach jeder Lease ``/run/tesm-dhcp-options.env``
|
||||||
|
schreibt -- "name=wert", lesbar. ``roh`` bleibt fuer den Fall, dass nur
|
||||||
|
die Datei von Hand kopiert wurde.
|
||||||
"""
|
"""
|
||||||
if not codes:
|
if not benannt:
|
||||||
return ""
|
return {}
|
||||||
code = codes[0]
|
code = int(benannt[0]["code"])
|
||||||
|
name = str(benannt[0]["name"])
|
||||||
if stil == "networkd":
|
if stil == "networkd":
|
||||||
return (
|
return {
|
||||||
|
"einfach": f"grep '^{name}=' /run/tesm-dhcp-options.env",
|
||||||
|
"roh": (
|
||||||
"lease=/run/systemd/netif/leases/$(cat /sys/class/net/$IF/ifindex); "
|
"lease=/run/systemd/netif/leases/$(cat /sys/class/net/$IF/ifindex); "
|
||||||
f"/usr/bin/printf \"$(sed -n 's/^OPTION_{code}=//p' \"$lease\" | sed 's/../\\\\x&/g')\""
|
f"/usr/bin/printf \"$(sed -n 's/^OPTION_{code}=//p' \"$lease\" "
|
||||||
)
|
"| sed 's/../@BS@x&/g')\""
|
||||||
|
).replace("@BS@", "\\" * 2),
|
||||||
|
}
|
||||||
if stil == "dhcpcd":
|
if stil == "dhcpcd":
|
||||||
return "dhcpcd -U $IF | grep -i '^url='"
|
# dhcpcd kennt den Namen, weil 'define' ihn ihm beigebracht hat.
|
||||||
return "grep -i 'option url' /var/lib/dhcp/dhclient.leases | tail -n1"
|
return {"einfach": f"dhcpcd -U $IF | grep -i '^{name}='", "roh": ""}
|
||||||
|
return {
|
||||||
|
"einfach": f"grep -i 'option {name}' /var/lib/dhcp/dhclient.leases | tail -n1",
|
||||||
|
"roh": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _uebernahme_skript(stil: str, inhalt: str) -> str:
|
def _b64(text: str) -> str:
|
||||||
|
import base64
|
||||||
|
|
||||||
|
return base64.b64encode(text.encode("utf-8")).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _uebernahme_skript(stil: str, inhalt: str, benannt: list[dict[str, Any]] | None = None) -> str:
|
||||||
"""Das Skript, das die Konfiguration auf dem Client eintraegt.
|
"""Das Skript, das die Konfiguration auf dem Client eintraegt.
|
||||||
|
|
||||||
Zwei Dinge macht es anders als ein blosses ``tee``, und beide sind mit
|
Zwei Dinge macht es anders als ein blosses ``tee``, und beide sind mit
|
||||||
@@ -948,12 +1150,24 @@ def _uebernahme_skript(stil: str, inhalt: str) -> str:
|
|||||||
Der Inhalt reist base64-kodiert und als **Argument**, nicht ueber stdin:
|
Der Inhalt reist base64-kodiert und als **Argument**, nicht ueber stdin:
|
||||||
dort steht bei einem Client mit Passwort das Kennwort fuer ``sudo``.
|
dort steht bei einem Client mit Passwort das Kennwort fuer ``sudo``.
|
||||||
"""
|
"""
|
||||||
import base64
|
fracht = _b64(inhalt)
|
||||||
|
benannt = benannt or []
|
||||||
fracht = base64.b64encode(inhalt.encode("utf-8")).decode("ascii")
|
|
||||||
|
|
||||||
if stil == "networkd":
|
if stil == "networkd":
|
||||||
return NETWORKD_SKRIPT.replace("@FRACHT@", fracht)
|
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))
|
||||||
|
.replace("@DIENST@", _b64(DIENST_EINHEIT))
|
||||||
|
.replace("@PFADEINHEIT@", _b64(PFAD_EINHEIT))
|
||||||
|
)
|
||||||
|
|
||||||
ziel = "/etc/dhcpcd.conf" if stil == "dhcpcd" else "/etc/dhcp/dhclient.conf"
|
ziel = "/etc/dhcpcd.conf" if stil == "dhcpcd" else "/etc/dhcp/dhclient.conf"
|
||||||
dienst = (
|
dienst = (
|
||||||
|
|||||||
@@ -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,16 @@
|
|||||||
<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 sendet keine eigene Option einen Wert)' }}
|
||||||
|
</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>
|
||||||
@@ -321,6 +344,8 @@
|
|||||||
<div class="card__title">{{ icon('plus', size=17) }} Eigene Option anlegen</div>
|
<div class="card__title">{{ icon('plus', size=17) }} Eigene Option anlegen</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Für alles jenseits des Katalogs – etwa eine URL, die der Client beim Start öffnet.
|
Für alles jenseits des Katalogs – etwa eine URL, die der Client beim Start öffnet.
|
||||||
|
Code <strong>224–254</strong>: nur diesen Bereich sieht DHCP für eigene Optionen vor,
|
||||||
|
und nur daraus hebt ein Linux-Client mit systemd-networkd sie überhaupt auf.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +353,8 @@
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ 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') }}
|
{{ field('name', 'Name', required=True, placeholder='meine-option') }}
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Typ</span>
|
<span class="field__label">Typ</span>
|
||||||
@@ -445,12 +471,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<pre class="code-block code-block--wrap" id="konfig-{{ stil }}">{{ konfig.inhalt }}</pre>
|
<pre class="code-block code-block--wrap" id="konfig-{{ stil }}">{{ konfig.inhalt }}</pre>
|
||||||
<div class="fs-xs faint">Danach: <code>{{ konfig.nachher }}</code></div>
|
<div class="fs-xs faint">Danach: <code>{{ konfig.nachher }}</code></div>
|
||||||
{% if konfig.auslesen %}
|
{% if konfig.auslesen.einfach %}
|
||||||
{# Ohne diese Zeile kann niemand pruefen, ob es angekommen ist --
|
{# Ohne diese Zeile kann niemand pruefen, ob es angekommen ist --
|
||||||
und ein RMM-Agent braucht genau sie. #}
|
und ein RMM-Agent braucht genau sie. #}
|
||||||
<div class="fs-xs faint">
|
<div class="fs-xs faint">
|
||||||
Auslesen auf dem Client: <code class="break-all">{{ konfig.auslesen }}</code>
|
Auslesen auf dem Client:
|
||||||
|
<code class="break-all">{{ konfig.auslesen.einfach }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
{% if konfig.auslesen.roh %}
|
||||||
|
<div class="fs-xs faint">
|
||||||
|
systemd legt fremde Optionen unter ihrer Nummer und hexkodiert ab – den
|
||||||
|
Klartext schreibt ein kleiner Übersetzer, den der SSH-Push mitinstalliert
|
||||||
|
(<code>/run/tesm-dhcp-options.env</code>, nach jeder Lease erneuert).
|
||||||
|
Direkt aus der Lease:
|
||||||
|
<code class="break-all">{{ konfig.auslesen.roh }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.has('servicing_command.execute') %}
|
{% if current_user.has('servicing_command.execute') %}
|
||||||
|
|||||||
Reference in new issue
Block a user