ConfigParser - Maschinell lesbare Konfigurationen von Netzwerkgeräten

Für einen schweizer Internet Provider entwickelte ich einen Parser zur Interpretierung, Normalisierung und Umwandlung von Netzwerkkonfigurationen.
Das Ziel: Eine Konfiguration in ein einheitliches, strukturiertes und maschinell lesbares Format überführen und via API bereitstellen.
Problemstellung
Die über die Jahre gewachsene, interne, von Hand gepflegte, technische CMDB des Kunden lieferte immer häufiger inkonsistente Daten. Dies führte zu diversen Problemen im täglichen Betrieb. Der Support sah sich gezwungen, die Daten vorab mit anderen Mitteln zu verifizieren.
Darüber hinaus fehlten diverse Informationen zu einem Netzwerkgerät wie beispielsweise die IPv6-Adressen. Dies machte es schwierig im Fehlerfall zeitnah zu reagieren, Workflows zu automatisieren und gewisse Tasks dem Endkunden im Self-Service anzubieten.
Lösung
Durch die Entwicklung eines Parsers für die Konfigurationen von Netzwerkgeräten (u.a. Cisco, Brocade, Fortigate, Barracuda), konnte ich die Daten aus den Live-Systemen extrahieren, aufbereiten, normalisieren, prüfen und via REST-API anderen Systemen in einem maschinell lesbaren Format zur Verfügung stellen.
Die Lösung ermöglichte es dem Kunden, die Daten der CMDB zu prüfen, in eine neue Lösung zu überführen und die digitale Transformation weiter voranzutreiben.
Umsetzung
Bei der Untersuchung der Netzwerk-Konfigurationen zeigte sich, dass fast alle Netzwerk-Hersteller die Daten in reinem Text vorhalten. Leider nicht in einem strukturierten Format wie JSON
oder XML
aber immerhin halbwegs strukturiert mittels Leerzeichen und anderen speziellen Zeichen wie beispielsweise einem Ausrufezeichen !
.
Nachfolgend ein gekürztes Beispiel einer Cisco IOS Router-Konfiguration von einem Interface und VRF (Daten sind frei erfunden):
...
!
interface TenGigabitEthernet1/1
description dummy-interface::1/5
mtu 1500
ip address 160.6.123.142 255.255.255.252
no ip redirects
no ip proxy-arp
ip flow ingress
ip ospf network point-to-point
ip ospf cost 25
logging event link-status
ipv6 address 288c:3ede:9c0e::D/127
ipv6 enable
ipv6 ospf network point-to-point
ipv6 ospf cost 25
ipv6 ospf 100 area 0
!
...
!
vrf VRF-DUMMY
rd 12345:000000
address-family ipv4
ip route 0.0.0.0/0 160.6.123.1
exit-address-family
exit-vrf
!
Die Software sollte zukünftig leicht erweitert werden. Gleichzeitig wollte ich die Daten der unterschiedlichen Hersteller miteinander vergleichen können. Es war daher zentral, die unterschiedlichen Daten zu normalisieren und auf eine Basis zu bringen.
Im Untergrund setzte ich konsequent auf dem SOLID
-Prinzip auf. Die Rohdaten wurden in Domain-spezifische Blöcke aufgeteilt (beispielsweise InterfaceBlock
, VrfBlock
, MacBlock
), welche entsprechende Interfaces implementierten. Daneben baute ich pro Hersteller und OS-Version eigene Parser.
Die Instanzierung des korrekten Parsers geschieht über eine ParserFactory
, welche den Typ anhand der ersten Zeile der Konfiguration ermittelt.
Der Programmablauf ist relativ simpel:
- Der Parser öffnet die ihm übergebene Datei und navigiert sich Zeile für Zeile bis zum Ende entlang durch.
- Ich habe mich bewusst dagegen entschieden, die komplette Datei in den Arbeitsspeicher zu laden. Die Lösung ist somit sehr performant und ich konnte mehrere tausend Konfigurationen in wenigen Sekunden umwandeln, ohne dabei massiv Arbeitsspeicher reservieren zu müssen.
- Auf jeder Zeile prüft der Parser mittels
RegEx
-Regeln, ob es sich um einen registrierten Block handelt:- Befinden wir uns in einem neuen Block, wird ein neues Objekt vom entsprechenden Block-Typ instanziert und an den Parser angehängt.
- Befinden wir uns in einem bestehenden Block, wird die dazugehörige Block-Instanz abgerufen.
- Das Block-Objekt wird daraufhin mit den Rohdaten befüllt, bis das Ende des Blockes erreicht ist. Das Ganze funktioniert auch mit verschachtelten Blöcken.
- Wurden das Ende erreicht und alle Blöcke mit den Rohdaten befüllt, werden diese nun durch den Einsatz von diversen RegEx-Regeln interpretiert, normalisiert und in die entsprechenden Klassen-Properties hydriert.
Da das BlockInterface
vom JsonSerializable
Interface erbt, ist es am Ende ein leichtes diese Daten in ein JSON-Format zu überführen.
Die Ausgabe vom obigen Beispiel sieht am Ende folgendermassen aus:
{
"filename": "router-xy",
"vendor": "cisco",
"os": "ios",
"version": 15.5,
"interfaces": [
{
"name": "TenGigabitEthernet1/1",
"description": "dummy-interface::1/5",
"mtu": 1500,
"ip_addresses": [
{
"ip": "160.6.123.142",
"cidr": "160.6.123.142/30",
"prefix": 30,
"start": "160.6.123.142",
"end": "160.6.123.145",
"version": 4,
"isPrivate": false
},
{
"ip": "288c:3ede:9c0e::x",
"cidr": "288c:3ede:9c0e::x/127",
"prefix": 127,
"start": "288c:3ede:9c0e::x",
"end": "288c:3ede:9c0e::y",
"version": 6,
"isPrivate": false
}
],
"...": "// Weitere Properties"
}
],
"vrfs": [
{
"name": "VRF-DUMMY",
"rd": "12345:000000",
"ip_route": {
"ip": "0.0.0.0/0",
"nextHop": "160.6.123.1"
}
}
]
}
Viele Regeln zum Parsen der Rohdaten überschneiden sich von Hersteller zu Hersteller. Daher habe ich diese im jeweiligen Interface definiert und können somit bei Bedarf in der Detail-Implementierung eines Blocks dank Vererbung übersteuert werden. Die Lösung ist somit sehr flexibel und kann bei etwaige Änderungen oder neuen Konfigurationstypen schnell angepasst werden.
Die Anwendung wurde in einem Microservice gekapselt, mit dem interne Git-System des Kunden verknüpft und eine REST-API erstellt, womit nun eine Konfiguration aus dem Git-Backup per API abgerufen und umgewandelt werden kann.
Meine Arbeiten
- Analyse: Verstehen des Aufbaus der unterschiedlichen Netzwerkkonfigurationen nach Hersteller, Gerätetyp und OS-Version.
- Konzeption: Erstellen eines geeigneten Software-Konzepts.
- Proof of Concept: Prüfen der Machbarkeit mittels eines PoCs.
- Umsetzung: Entwicklung der Software als Microservice mit dazugehöriger REST-API.
- Testing: Erstellung einer Vielzahl an Unit-Tests nach Hersteller und Konfigurationstyp.
- Deployment: Bereistellung als leichtgewichtiger Container auf einem internen Kubernetes-Cluster.
- Dokumentation: Festhalten der Software-Architektur, des Programmablaufs, aller Interfaces und der API.
Tech-Stack
- REST-API / Backend:
- Laminas und Laminas\Mezzio
- laminas-di und laminas-servicemanager zur Dependency Injection und als Instanzmanager.
- laminas-filter, laminas-validator, laminas-inputfilter zur Filterung und Validierung der API-Endpoints und der übermittelten Daten.
- laminas-hydrator zur Hydrierung und De-Hydrierung der Daten/Objekte.
- laminas-http zur Anbindung des Git-Repositories.
- laminas-diactoros für PSR kompatible HTTP Request- und Response-Objekte.
- laminas-log fürs Logging mit entsprechender Middleware für AMQP.
- laminas-cache für einen Object- und Mem-Cache.
- IP-Lib von Michele Locati zur Normalisierung der IP-Daten (Herzlichen Dank für die tolle Arbeit!)
- mezzio-hal zur Generierung von API-Links innerhalb der JSON-Response (bspw. für die Paginierung) im Hypertext Application Language (HAL) Format.
- mezzio-problem-details für standardisierte, domainspezifische Exceptions gemäss PSR.
- Frontend: AngularJs sowie HTML5, CSS3
- Deployment: Als Container auf dem internen Kubernetes-Cluster des Kunden
Fazit
Ein herausforderndes Projekt mit klaren Anforderungen, welches tiefe RegEx- und Netzwerk-Kenntnisse erforderte. Das Ergebnis kann sich sehen lassen. Der Kunde konnte seine veralteten Daten verifizieren, Abläufe automatisieren und diverse Tasks im Self-Service anbieten.