Compare commits

..
2 Commits
6 changed files with 145 additions and 25 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.43" version = "2.8.45"
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 = [
+7 -11
View File
@@ -11,7 +11,7 @@ einen Dienst starten.
from __future__ import annotations from __future__ import annotations
import base64 import shlex
from typing import Any from typing import Any
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
@@ -684,16 +684,12 @@ def push_client_config():
flash("Es wurde kein Gerät ausgewählt.", "warning") flash("Es wurde kein Gerät ausgewählt.", "warning")
return redirect(url_for("dhcp.overview")) return redirect(url_for("dhcp.overview"))
# Ueber base64 statt ueber ein Here-Dokument: der Inhalt enthaelt # Ein Skript, ein Aufruf. Der frühere Weg schrieb auf einen festen Pfad --
# Anfuehrungszeichen, Rauten und Zeilenumbrueche, und er geht durch SSH, # bei networkd auf einen, den kein Dienst liest -- und überschrieb bei
# eine Shell und moeglicherweise sudo. Was kodiert reist, kommt an. # dhcpcd/dhclient die Datei des Clients. Das Skript sucht sich das Ziel
inhalt = base64.b64encode(konfig["inhalt"].encode("utf-8")).decode("ascii") # selbst und ersetzt nur seinen eigenen Block; es geht als **Argument**
verzeichnis = str(konfig["pfad"]).rsplit("/", 1)[0] # durch, damit stdin für das sudo-Passwort frei bleibt.
befehl = ( befehl = "{sudo} sh -c " + shlex.quote(konfig["skript"])
f"{{sudo}} mkdir -p {verzeichnis} && "
f"echo {inhalt} | base64 -d | {{sudo}} tee {konfig['pfad']} >/dev/null && "
f"{{sudo}} sh -c '{konfig['nachher']}' || true"
)
gestartet = 0 gestartet = 0
for device_id in device_ids: for device_id in device_ids:
+118 -11
View File
@@ -746,7 +746,11 @@ IMMER_ANGEFRAGT = frozenset({1, 3, 6, 12, 15, 28, 42, 51, 54, 58, 59, 119, 121})
#: * **dhclient** -- Debian 11 bis 13 und Ubuntu 22.04, dort wo isc-dhcp-client #: * **dhclient** -- Debian 11 bis 13 und Ubuntu 22.04, dort wo isc-dhcp-client
#: noch installiert ist. #: noch installiert ist.
CLIENT_STILE: tuple[tuple[str, str, str], ...] = ( CLIENT_STILE: tuple[tuple[str, str, str], ...] = (
("networkd", "systemd-networkd", "/etc/systemd/network/10-tesm.network.d/50-tesm.conf"), # Der Zusatz gehoert zu der .network-Datei, die fuer die Schnittstelle
# wirklich gilt -- bei netplan heisst die 10-netplan-<if>.network und liegt
# unter /run. Ein Verzeichnis "10-tesm.network.d" liest niemand: es gibt
# keine 10-tesm.network. Den Namen ermittelt das Skript auf dem Client.
("networkd", "systemd-networkd", "/etc/systemd/network/<aktive>.network.d/50-tesm.conf"),
("dhcpcd", "dhcpcd", "/etc/dhcpcd.conf"), ("dhcpcd", "dhcpcd", "/etc/dhcpcd.conf"),
("dhclient", "dhclient (isc-dhcp-client)", "/etc/dhcp/dhclient.conf"), ("dhclient", "dhclient (isc-dhcp-client)", "/etc/dhcp/dhclient.conf"),
) )
@@ -789,6 +793,40 @@ def gesendete_optionen(conn: sqlite3.Connection) -> list[dict[str, Any]]:
) )
#: Traegt den Zusatz dort ein, wo networkd ihn auch liest.
NETWORKD_SKRIPT = """set -e
schnittstelle=$(ip -o -4 route show default 2>/dev/null | awk '{print $5; exit}')
[ -n "$schnittstelle" ] || { echo "Keine Standardroute -- Schnittstelle unbekannt." >&2; exit 1; }
datei=$(COLUMNS=400 SYSTEMD_COLORS=0 networkctl status "$schnittstelle" 2>/dev/null | sed -n 's/^ *Network File: *//p' | head -n1)
case "${datei:-}" in
""|n/a|*not*) datei=$(grep -ls "Name=$schnittstelle" /run/systemd/network/*.network /etc/systemd/network/*.network 2>/dev/null | head -n1) ;;
esac
[ -n "${datei:-}" ] || { echo "Keine .network-Datei fuer $schnittstelle gefunden." >&2; exit 1; }
ziel="/etc/systemd/network/$(basename "$datei").d/50-tesm.conf"
mkdir -p "$(dirname "$ziel")"
printf %s '@FRACHT@' | base64 -d > "$ziel"
chmod 0644 "$ziel"
echo "geschrieben: $ziel"
networkctl reload
networkctl reconfigure "$schnittstelle"
echo "Lease: /run/systemd/netif/leases/$(cat /sys/class/net/"$schnittstelle"/ifindex)"
"""
#: Ersetzt den eigenen Block in einer Datei, die dem Client gehoert.
BLOCK_SKRIPT = """set -e
ziel=@ZIEL@
mkdir -p "$(dirname "$ziel")"
[ -f "$ziel" ] || : > "$ziel"
zwischen=$(mktemp)
sed '/^# >>> TESM/,/^# <<< TESM/d' "$ziel" > "$zwischen"
printf %s '@FRACHT@' | base64 -d >> "$zwischen"
cat "$zwischen" > "$ziel"
rm -f "$zwischen"
echo "ergaenzt: $ziel"
@DIENST@
"""
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.
@@ -798,8 +836,10 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
ausdruecklich anfordern. Ohne diese Zeilen sendet der Server brav und der ausdruecklich anfordern. Ohne diese Zeilen sendet der Server brav und der
Client wirft es weg, ohne dass irgendwo etwas danebengeht. Client wirft es weg, ohne dass irgendwo etwas danebengeht.
Rueckgabe: ``{"pfad", "inhalt", "nachher", "optionen"}`` -- Datei, Inhalt, Rueckgabe: ``{"pfad", "inhalt", "nachher", "skript", "optionen"}``.
der Befehl zum Uebernehmen und die Optionen, um die es geht. ``skript`` ist die Fassung zum Ausfuehren -- sie sucht sich das Ziel auf
dem Client selbst und traegt den Inhalt ein, ohne Vorhandenes zu
verlieren.
""" """
optionen = [ optionen = [
eintrag eintrag
@@ -814,20 +854,22 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
"# Von TESM erzeugt. Fordert die Optionen an, die dieser Server sendet --", "# Von TESM erzeugt. Fordert die Optionen an, die dieser Server sendet --",
"# ohne diese Zeilen verwirft der Client sie stillschweigend.", "# ohne diese Zeilen verwirft der Client sie stillschweigend.",
"#", "#",
"# Der Dateiname muss zur .network-Datei der Schnittstelle passen:", "# Diese Datei ist ein Zusatz zu der .network-Datei, die fuer die",
"# netplan legt sie unter /run/systemd/network/10-netplan-<if>.network ab,", "# Schnittstelle gilt. Wie sie heisst, sagt 'networkctl status <if>';",
"# der Zusatz gehoert also nach", "# bei netplan ist es /run/systemd/network/10-netplan-<if>.network, der",
"# Zusatz gehoert dann nach",
"# /etc/systemd/network/10-netplan-<if>.network.d/50-tesm.conf.", "# /etc/systemd/network/10-netplan-<if>.network.d/50-tesm.conf.",
"#", "#",
"# Die Werte stehen danach in /run/systemd/netif/leases/<ifindex>", "# Die Werte stehen danach in /run/systemd/netif/leases/<ifindex>",
"# als OPTION_<code>=...", "# als OPTION_<code>= -- hexkodiert, weil systemd fremde Optionen nicht",
"# deutet.",
"[DHCPv4]", "[DHCPv4]",
] ]
zeilen.append("RequestOptions=" + " ".join(str(code) for code in codes)) zeilen.append("RequestOptions=" + " ".join(str(code) for code in codes))
nachher = "networkctl reload && networkctl reconfigure <schnittstelle>" nachher = "networkctl reload && networkctl reconfigure <Schnittstelle>"
elif stil == "dhcpcd": elif stil == "dhcpcd":
zeilen = [ zeilen = [
"# Von TESM erzeugt -- an das Ende von /etc/dhcpcd.conf.", "# >>> TESM -- nicht von Hand aendern, wird beim naechsten Mal ersetzt",
"# 'define' macht die Option bekannt, 'option' fordert sie an.", "# 'define' macht die Option bekannt, 'option' fordert sie an.",
"# Danach steht der Wert in der Lease (/var/lib/dhcpcd/<if>.lease)", "# Danach steht der Wert in der Lease (/var/lib/dhcpcd/<if>.lease)",
"# und laesst sich mit 'dhcpcd -U <if>' als Variable auslesen.", "# und laesst sich mit 'dhcpcd -U <if>' als Variable auslesen.",
@@ -837,10 +879,11 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
zeilen.append(f"define {eintrag['code']} {typ} {eintrag['name']}") zeilen.append(f"define {eintrag['code']} {typ} {eintrag['name']}")
for eintrag in optionen: for eintrag in optionen:
zeilen.append(f"option {eintrag['name']}") zeilen.append(f"option {eintrag['name']}")
zeilen.append("# <<< TESM")
nachher = "systemctl restart dhcpcd" nachher = "systemctl restart dhcpcd"
else: else:
zeilen = [ zeilen = [
"# Von TESM erzeugt -- an das Ende von /etc/dhcp/dhclient.conf.", "# >>> TESM -- nicht von Hand aendern, wird beim naechsten Mal ersetzt",
"# Die Werte stehen danach in /var/lib/dhcp/dhclient.leases,", "# Die Werte stehen danach in /var/lib/dhcp/dhclient.leases,",
"# und in Hooks unter $new_<name>.", "# und in Hooks unter $new_<name>.",
] ]
@@ -851,16 +894,80 @@ def client_konfiguration(conn: sqlite3.Connection, stil: str) -> dict[str, Any]:
zeilen.append( zeilen.append(
"also request " + ", ".join(str(eintrag["name"]) for eintrag in optionen) + ";" "also request " + ", ".join(str(eintrag["name"]) for eintrag in optionen) + ";"
) )
zeilen.append("# <<< TESM")
nachher = "systemctl restart networking || dhclient -r && dhclient" nachher = "systemctl restart networking || dhclient -r && dhclient"
inhalt = chr(10).join(zeilen) + chr(10)
return { return {
"pfad": pfad, "pfad": pfad,
"inhalt": chr(10).join(zeilen) + chr(10), "inhalt": inhalt,
"nachher": nachher, "nachher": nachher,
"skript": _uebernahme_skript(stil, inhalt),
"auslesen": _auslesen(stil, codes),
"optionen": optionen, "optionen": optionen,
} }
def _auslesen(stil: str, codes: list[int]) -> str:
"""Wie der Wert auf dem Client wieder herauskommt.
Gehoert dazu, weil "angekommen" sonst niemand pruefen kann -- und weil ein
RMM-Agent genau diese Zeile braucht. Bei systemd-networkd steht der Wert
**hexkodiert** in der Lease: systemd kennt fremde Optionen nicht und legt
sie roh ab. Ohne das Umrechnen liest man 687474703a... und haelt es fuer
Unsinn.
"""
if not codes:
return ""
code = codes[0]
if stil == "networkd":
return (
"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')\""
)
if stil == "dhcpcd":
return "dhcpcd -U $IF | grep -i '^url='"
return "grep -i 'option url' /var/lib/dhcp/dhclient.leases | tail -n1"
def _uebernahme_skript(stil: str, inhalt: str) -> str:
"""Das Skript, das die Konfiguration auf dem Client eintraegt.
Zwei Dinge macht es anders als ein blosses ``tee``, und beide sind mit
Schaden bezahlt worden:
* Bei ``networkd`` **sucht es das Ziel auf dem Client**. Der Name des
Zusatzverzeichnisses muss zur aktiven .network-Datei passen; ein fester
Pfad legt eine Datei an, die kein Dienst je liest -- sie stand da, und
die Option fehlte trotzdem in der Lease.
* Bei ``dhcpcd`` und ``dhclient`` gehoeren die Zeilen in eine Datei, die
dem Client gehoert. Sie zu ueberschreiben haette seine uebrige
Konfiguration geloescht. Stattdessen wird nur der eigene Block zwischen
den Marken ersetzt -- beliebig oft wiederholbar.
Der Inhalt reist base64-kodiert und als **Argument**, nicht ueber stdin:
dort steht bei einem Client mit Passwort das Kennwort fuer ``sudo``.
"""
import base64
fracht = base64.b64encode(inhalt.encode("utf-8")).decode("ascii")
if stil == "networkd":
return NETWORKD_SKRIPT.replace("@FRACHT@", fracht)
ziel = "/etc/dhcpcd.conf" if stil == "dhcpcd" else "/etc/dhcp/dhclient.conf"
dienst = (
"systemctl restart dhcpcd"
if stil == "dhcpcd"
else "systemctl restart networking 2>/dev/null || { dhclient -r || true; dhclient; }"
)
return (
BLOCK_SKRIPT.replace("@ZIEL@", ziel)
.replace("@FRACHT@", fracht)
.replace("@DIENST@", dienst)
)
def service_state(sysops: SysOps) -> dict[str, str]: def service_state(sysops: SysOps) -> dict[str, str]:
state = sysops.service_state(SERVICE_UNIT) state = sysops.service_state(SERVICE_UNIT)
import shutil import shutil
@@ -445,6 +445,13 @@
</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 %}
{# Ohne diese Zeile kann niemand pruefen, ob es angekommen ist --
und ein RMM-Agent braucht genau sie. #}
<div class="fs-xs faint">
Auslesen auf dem Client: <code class="break-all">{{ konfig.auslesen }}</code>
</div>
{% endif %}
{% if current_user.has('servicing_command.execute') %} {% if current_user.has('servicing_command.execute') %}
<form method="post" action="{{ url_for('dhcp.push_client_config') }}"> <form method="post" action="{{ url_for('dhcp.push_client_config') }}">
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.8.21" version = "2.8.22"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -657,9 +657,19 @@
gap: var(--sp-5); gap: var(--sp-5);
} }
/* Bleibt stehen wie die Kopfzeile: Fassung und angemeldeter Benutzer sind
Angaben, nach denen man mitten in einer langen Seite fragt -- und bis
hierher zu scrollen, nur um zu sehen, welche Fassung laeuft, ist eine
Zumutung. Derselbe milchige Untergrund wie oben, damit Inhalt, der
darunter durchlaeuft, nicht mitliest. */
.page-footer { .page-footer {
padding: var(--sp-4) var(--sp-5); position: sticky;
bottom: 0;
z-index: var(--z-sticky);
padding: var(--sp-3) var(--sp-5);
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
background: color-mix(in srgb, var(--canvas) 82%, transparent);
backdrop-filter: blur(12px) saturate(1.4);
color: var(--text-faint); color: var(--text-faint);
font-size: var(--fs-xs); font-size: var(--fs-xs);
display: flex; display: flex;