Die Adresse meines Monitoring-Servers stand an 24 Stellen: so viele Dateien allein in der Doku nannten sie, als ich nachgezählt habe. Dazu kamen acht handgepflegte Listen, die Dienste und Hosts jeweils neu aufzählten: Intranet-Kacheln, Zertifikatsziele, Erfassungsstand, Sicherungsplan, Abhängigkeiten und weitere. Jede Änderung musste an mehreren Orten nachgezogen werden, und irgendeiner wurde immer vergessen. Heute pflege ich diese Angaben in genau einer YAML-Datei, dem Asset-Register. Daraus entstehen Doku-Tabellen, Intranet-Kacheln, Zertifikatsziele und IP-Tabellen.

Die Ausgangslage
Bevor ich etwas gebaut habe, habe ich gezählt, wo welche Angabe gepflegt wird:
| Schicht | Beispiele | Pflege |
|---|---|---|
| Live-Systeme | Proxmox, DHCP, DNS, Monitoring, Backup-Server | sind die Wahrheit |
| automatisch erzeugt | Inventar aus Proxmox, DHCP und Monitoring, daraus 31 Seiten | keine |
| von Hand, strukturiert | 8 Listen, die Dienste oder Hosts neu aufzählen | doppelt |
| von Hand, Fließtext | 48 von 71 handgeschriebenen Seiten mit festen Adressen | doppelt, veraltet schnell |
Die erste Zeile war nie das Problem. Was Proxmox oder der DHCP-Server weiß, lese ich ohnehin täglich per Skript aus. Das Problem waren die beiden unteren Zeilen. Sie enthalten das, was sich nicht ablesen lässt, und das stand an vielen Orten gleichzeitig.
Absicht im Register, Fakten aus dem Live-System
Die wichtigste Entscheidung war die Abgrenzung. Ins Register gehört nur, was ich will: Rolle, Soll-Adresse, Soll-Name, Dienste und Ports, Kritikalität, Backup-Pflicht, Kachel im Intranet, Zertifikatsziel, Doku-Link. Was ein System selbst weiß, lese ich dort aus und schreibe es nicht ab: ob eine VM existiert, auf welchem Knoten sie läuft, welche Leases vergeben sind, ob ein Host im Monitoring grün ist.
Zwei Dinge stehen bewusst nicht im Register. Die MAC-Adressen bleiben in den DHCP-Reservierungen, denn dort ist ihre Wahrheit. Und Geheimnisse stehen nirgends, nur Orte und Namen.
Der Aufbau der Datei
Ich habe mich für eine einzige Datei entschieden, nicht für einen Ordner mit vielen kleinen. Der Prüfer liest sie als Ganzes, und ein Diff zeigt jede Änderung an einer Stelle. Nach dem ersten Import hatte sie 116 Assets und rund 1.200 Zeilen. Vereinfacht und mit Beispieldaten sieht sie so aus:
gruppen:
- {id: infra, name: Infrastruktur}
- {id: haus, name: Haus}
assets:
monitor:
typ: vm # host | vm | ct | geraet
vmid: 120 # Verbindung zum Proxmox-Inventar
name: monitor.example.lan
rolle: "Monitoring und Doku"
ip: 192.0.2.10 # Soll, wird gegen DHCP, DNS, Proxmox geprüft
dns: [monitor, docs]
ssh: monitor
kritikalitaet: hoch
backup: {pbs: true, konfig: true}
zertifikate:
- {id: Z01, fqdn: monitor.example.lan, handler: apache}
steckdose-kueche: {typ: geraet, name: steckdose-kueche, ip: 192.0.2.85, art: shelly, web: true}
dienste:
monitoring:
asset: monitor
name: Monitoring
gruppe: infra
url: https://monitor.example.lan/
kachel: true # erscheint im Intranet
adressplan_bloecke:
- {von: 80, bis: 109, zweck: Schaltaktoren}
Server und Gäste haben ausführliche Einträge. Die rund 90 IoT-Geräte stehen einzeln, aber kompakt in einer Zeile mit Name, Adresse, Art und gegebenenfalls Raum. Ich hätte sie auch als Gruppen führen können. Ich wollte sie aber einzeln, damit jedes Gerät mit Weboberfläche eine eigene Kachel im Intranet bekommt.
Den Anfang hat ein Importskript gemacht. Es hat das Register einmalig aus allen bestehenden Listen zusammengesetzt und Widersprüche aufgelistet, die ich von Hand geklärt habe. Seitdem pflege ich nur noch das Register.
Was daraus entsteht
Ein Generator läuft als erster Schritt jedes Doku-Baus. Er schreibt die Kacheln des Intranets mit Gruppen und Downloads, die Zielliste für den Zertifikatsautomaten mit Namen, Ports und Handler je Ziel, die IP-Tabelle im Netzplan und die Tabelle der Adressblöcke mit belegten und freien Adressen, den Erfassungsstand der Doku mit der Abhängigkeitsliste und einen Vorschlag für die SSH-Konfiguration. Der Vorschlag steht in einer eigenen Datei, die echte Konfiguration fasst er nicht an.
In Doku-Seiten stehen Adressen als Platzhalter wie {{ip:monitor}}, die ein Hook beim Bau aus dem Register füllt.
Vor dem Schreiben prüft der Generator das Register: Ist jeder Typ bekannt? Ist eine Adresse doppelt vergeben? Verweist jeder Dienst auf ein vorhandenes Asset und eine vorhandene Gruppe? Schlägt eine Prüfung fehl, schreibt er gar nichts. Eine halb erzeugte Zertifikatsliste ist schlimmer als eine veraltete.

Konflikterkennung: keine Handänderung geht verloren
Jede erzeugte Datei trägt einen Kopf „ERZEUGT … nicht von Hand ändern“. Das hält niemanden ab, schon gar nicht eine zweite KI-Sitzung, die schnell etwas korrigieren will. Deshalb merkt sich der Generator nach jedem Schreiben eine Prüfsumme der Datei. Beim nächsten Lauf vergleicht er:
neu = sha256(erzeugter_text)
if datei_existiert:
alt = sha256(datei_inhalt)
if alt == neu:
return "unverändert"
if alt != gemerkt.get(pfad): # seit dem letzten Schreiben geändert
return "KONFLIKT" # Datei bleibt, wie sie ist
schreibe(erzeugter_text)
gemerkt[pfad] = neu
Bei einem Konflikt überschreibt er die Datei nicht, sondern meldet ihn und lässt sie stehen. So verschwindet keine Änderung stillschweigend, und die Meldung sagt gleich, wo die Änderung hingehört: ins Register. Bei Seiten, in denen nur ein Block erzeugt ist, etwa die Tabelle im Netzplan, gilt die Prüfsumme nur für den Block zwischen zwei Markierungen. Anfangs galt sie für die ganze Seite. Damit hätte jede Korrektur im Fließtext einen Konflikt ausgelöst.
Der tägliche Abgleich mit der Wirklichkeit
Ein zweites Skript prüft täglich und nur lesend, ob Register und Live-Systeme zusammenpassen. Es unterscheidet Abweichungen (etwas ist falsch) und Hinweise (jemand sollte es ansehen). Einige Beispiele:
- Ein Gast in Proxmox hat keinen Eintrag im Register, oder umgekehrt.
- Die Soll-Adresse weicht von der DHCP-Reservierung, dem DNS-Eintrag oder der ARP-Tabelle ab.
- Ein Gast soll gesichert werden, steht aber nicht im Backup-Job.
- Ein Gerät antwortet auf Webports, ist aber nicht im Register.
- Eine erzeugte Datei hat ihren Kopf verloren, wurde also vermutlich von Hand ersetzt.
Das Ergebnis landet auf einer Doku-Seite und als Dienst im Monitoring: OK bei null Abweichungen, Warnung ab einer, kritisch ab sechs oder wenn der letzte Abgleich älter als 54 Stunden ist.
Was das Register gefunden hat
Der erste Abgleich meldete drei Abweichungen und fünf Hinweise. Drei DNS-Namen aus der Zertifikatsplanung existierten gar nicht. Es gab mögliche Dubletten, etwa zwei Einträge für dasselbe Gerät vor und nach einem Umzug. Und ein Server fehlte im Monitoring.
Lehrreicher waren die Vergleichsproben. Bevor ich eine Liste auf den Generator umgestellt habe, musste die erzeugte Datei der handgepflegten entsprechen. Dabei kamen alte Fehler ans Licht:
- In der Abhängigkeitsliste war ein Name an einem Komma abgeschnitten.
- Bei der Zertifikatsliste zeigte die Probe einen Einrückungsfehler im DNS-Abschnitt. Die Probe hatte nur die berechneten Daten verglichen, nicht den geschriebenen Text. Seitdem liest sie die geschriebene Datei zurück.
- Als die Tabelle der Adressblöcke zum ersten Mal erzeugt wurde, fehlte ein Zählerleser im Register. Er trägt seine Adresse fest im Gerät und hat keine DHCP-Reservierung, aus der der Import ihn hätte übernehmen können.
- Zwölf Gastadressen kamen nur aus einem handgeschriebenen Netzplan, weil Proxmox sie ohne Gast-Agent nicht liefert. Heute liest der Abgleich die MAC aus der Gast-Konfiguration und sucht sie in der ARP-Tabelle. Alle 16 laufenden Gäste sind so bestätigt.
Neues Gerät per Merge-Request
Das Register liegt in Git, und Änderungen gehen per Merge-Request. Meldet jemand im Intranet ein neues Gerät, legt ein Skript daraus einen Entwurf an. Es sucht eine freie Adresse im Block der Geräteart, ergänzt den Eintrag im Register und öffnet einen Merge-Request. Die Befehle für die DHCP-Reservierung auf beiden Servern stehen in der Beschreibung. Mergen und reservieren mache ich selbst. Das Skript mergt nie.
Nach dem Merge prüft eine Pipeline drei Dinge: gültiges YAML und gültige Skripte, ob die erzeugten Listen zum Register passen, und ob versehentlich ein Schlüssel oder Kennwort im Repository gelandet ist. Den Doku-Bau selbst macht weiter der tägliche Lauf, weil auf dem Runner kein MkDocs installiert ist.
Technisch besteht das Ganze aus einer YAML-Datei, zwei Python-Skripten und einer Pipeline. Ein neuer Dienst braucht nur noch einen Eintrag, Kachel, Zertifikatsziel, Tabellenzeile und Abgleich folgen daraus. Offen ist Fleißarbeit: Viele IoT-Geräte tragen kryptische DHCP-Namen, und ein paar mögliche Dubletten muss ich noch klären.
Schreibe einen Kommentar