Die Kosten der Komplexität: Ansible AWX
05 May 2024
Konfiguration von NTP auf Cisco IOS mit Ansible
Dieser Artikel dient als Beispiel zur Erkundung verschiedener Strategien zur Konfiguration eines Features auf Netzwerkgeräten unter Verwendung von Ansible. Konkret werden wir uns auf die Konfiguration von NTP auf Cisco IOS-Geräten konzentrieren, obwohl die Diskussionen hier verallgemeinert werden können.
Wie bereits erwähnt, gibt es im Allgemeinen zwei mögliche Ansätze:
- Wir können spezifische Ansible-Module verwenden, insbesondere
cisco.ios.ios_ntp_global
; - Wir können das generische Konfigurationsmodul
cisco.ios.ios_config
verwenden.
Die beiden Ansätze unterscheiden sich grundlegend:
- Die Verwendung spezifischer Module delegiert alle Überprüfungen hinsichtlich Idempotenz, Integrität und Sauberkeit der Konfiguration an das Modul selbst. Während das Modul die gesamte Arbeit erledigt, müssen wir sicherstellen, dass es sich korrekt verhält (fehlerfrei), alle benötigten Einstellungen unterstützt und insbesondere keine Optimierungen vornehmen können.
- Die Verwendung generischer Konfigurationsmodule ermöglicht es uns, beliebige erforderliche Parameter zu konfigurieren (sehr nützlich bei komplexen Konfigurationen, die beispielsweise BGP, MPLS, QoS usw. umfassen), erfordert jedoch, dass wir Idempotenz, Sauberkeit und insbesondere Optimierung korrekt verwalten.
Lassen Sie uns im Detail sehen, wie NTP auf Cisco IOS in den beiden beschriebenen Modi konfiguriert wird, beginnend mit einer Liste von auf Konfigurationsebene definierten NTP-Peers:
ntp_peers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
- 3.pool.ntp.org
- 4.pool.ntp.org
Verwendung des Moduls cisco.ios.ios_ntp_global
module
Bei der Verwendung von Modulen, mit denen wir nicht vertraut sind, ist es immer ratsam:
- Überprüfen Sie die Moduldokumentation;
- Führen Sie Tests durch, um das Verhalten in allen möglichen Szenarien zu überprüfen;
- Stellen Sie sicher, dass sich das Verhalten nach Upgrades geändert hat.
Eine Überprüfung der Dokumentation zeigt sofort, dass das Modul die Liste der Peers in einem anderen Format als unseres benötigt. Da alle Parameter standardisiert sind, macht es aus designtechnischer Sicht Sinn, die Liste der NTP-Peers wie zuvor gesehen beizubehalten und die “Übersetzung” des Formats in die Rolle zu verschieben:
- ansible.builtin.set_fact:
ntp_peers_config_list: "{{ ntp_peers_config_list|default([]) + [{'peer': item}] }}"
with_items: "{{ ntp_peers }}"
Wir werden sofort feststellen, dass wir ein Leistungsproblem einführen, das wir später angehen werden.
An diesem Punkt haben wir eine Liste von NTP-Peers im richtigen Format, alles, was wir tun müssen, ist das Modul aufzurufen:
- cisco.ios.ios_ntp_global:
config:
peers: "{{ ntp_peers_config_list }}"
state: replaced
Der einzige offene Punkt ist der state
-Parameter, der verschiedene Werte annehmen kann:
merged
: Die hinzugefügte Konfiguration wird zu dem hinzugefügt, was bereits vorhanden ist (bereits vorhandene NTP-Peers werden nicht entfernt).replaced
undoverridden
: Die hinzugefügte Konfiguration ersetzt, was bereits vorhanden ist (bereits vorhandene NTP-Peers werden entfernt).deleted
: Die vorhandene Konfiguration wird entfernt, und es wird nichts konfiguriert.
In diesem Fall sind die Modi replaced
und overridden
identisch. Im Allgemeinen sollte das Verhalten immer mit der entsprechenden Dokumentation überprüft werden, aber wir können sagen, dass:
replaced
: Individuelle Konfigurationen, die im Gerät vorhanden sind und nicht erwartet werden, werden zuerst entfernt und dann korrekt wieder eingefügt.overridden
: Die gesamte Konfiguration, die im Gerät vorhanden ist, wird gelöscht und dann korrekt wieder eingefügt.
Wenn im Modul cisco.ios.ios_config
das Verhalten identisch ist, unterscheidet sich im Modul cisco.ios.ios_acls
s das Verhalten, weil ACLs aus Teilblöcken bestehen:
replaced
Entfernt in diesem Fall einzelne ACEs (Zugriffssteuerungseinträge) aus jeder ACL und fügt dann die richtigen ACEs ein.overridden
Entfernt in diesem Fall die ACLs und fügt sie dann korrekt wieder ein.
Es gibt dann drei Werte, die den Zustand der Konfiguration nicht ändern und für andere Bearbeitungen wie Debugging verwendet werden können:
rendered
: Die hinzugefügte Konfiguration wird im Ausgabedatei im Schlüsselrendered
gespeichert.gathered
: Die Konfiguration auf dem Gerät wird gelesen und im Ausgabedatei im JSON-Format unter Verwendung des Schlüsselsgathered
gespeichert.parsed
: Ähnlich wiegathered
, aber die Eingabe ist nicht die Gerätekonfiguration, sondern das, was imrunning_config
-Variablen vorhanden ist. Die Ausgabe wird im JSON-Format unter Verwendung des Schlüsselsparsed
gespeichert.
Angesichts der Komplexität der Konfigurationen, die auf Netzwerkgeräten durchgeführt werden können, sollte klar sein, warum es wichtig ist, zu überprüfen, ob das Modul wie erwartet funktioniert, die von uns benötigten Konfigurationen unterstützt und das Verhalten bei Updates gleich bleibt.
Die Konfiguration wird nicht automatisch am Ende der Aufgabe gespeichert: Es ist klar, dass in einem Playbook, das Dutzende verschiedener Module aufruft, wenn die Konfiguration nach jeder Aufgabe gespeichert würde, wir ein mögliches Leistungsproblem hätten. Darüber hinaus müssen wir bedenken, dass wir effektiv Text-Scraping durchführen, die Ausführung jedes Moduls das Lesen der Konfiguration unter Verwendung des Befehls show running-config
beinhaltet: Dies kann ein zusätzliches Leistungsproblem verursachen.
Verwendung des generischen Moduls
Das generische Modul cisco.ios.ios_config
gibt uns die volle Freiheit, jede Konfiguration hinzuzufügen und zu entfernen, die wir uns vorstellen können. Es liegt an uns, falls wir es für angemessen halten, Idempotenz, Korrektheit und Effizienz zu verwalten.
Das Modul cisco.ios.ios_config
:
- Wie im vorherigen Fall, führt automatisch den Befehl
show running-config
aus. Die Ausgabe wird verwendet, um zu überprüfen, ob bestimmte Befehle bereits vorhanden sind und beeinflusst somit stark denchanged
-Zustand. Wenn wir beispielsweiseint e0/0
konfigurieren, wird bei jeder Ausführung von Ansible die Konfiguration erneut eingegeben, weilinterface Ethernet0/0
stattdessen in der Konfiguration vorhanden ist. Daraus schließen wir, dass wir äußerst präzise in den Befehlen sein müssen, die wir ausführen. Bei jeder Änderung benachrichtigt uns Ansible mit der folgenden Nachricht: Um Idempotenz und eine korrekte Differenz zu gewährleisten, sollten die Eingabekonfigurationszeilen ähnlich sein wie sie erscheinen, wenn sie in der Laufkonfiguration auf dem Gerät vorhanden sind. - Im Gegensatz zum vorherigen Fall kann die Konfiguration aus der
running_config
-Variablen gelesen werden, was es uns ermöglicht, komplexe Playbooks zu optimieren. - Aufgrund der Funktionsweise von Cisco-Geräten werden Standardbefehle nicht in der Konfiguration angezeigt. Das Einfügen oder besser das Wiederherstellen eines Parameters in sein Standardverhalten bedeutet, dass Ansible immer den
changed
-Zustand erklärt. Dies liegt daran, dass der eingefügte (Standard-)Befehl niemals als vorhanden in der Konfiguration überprüft wird. Daher müssen wir dieses Ereignis manuell verwalten. - Aufgrund der Funktionsweise von Cisco-Geräten kann ich keine Liste von NTP-Peers deklarieren, sondern ich muss die fehlenden hinzufügen und die notwendigen entfernen. Im Gegensatz zum vorherigen Fall liegt es an uns, dieses Verhalten richtig zu implementieren.
Ein vereinfachter Ansatz, den ich “blind” nenne, besteht darin, Befehle trotzdem auszuführen, auch wenn sie nicht notwendig sind, z. B. das Entfernen eines bestimmten NTP-Peers. Dieser Ansatz hat sicherlich den Vorteil, einfach und sofortig zu sein, er erfordert jedoch, alle möglichen Fälle vorauszusehen und führt immer zum changed
-Zustand, was die Möglichkeit der Überprüfung der “Compliance” zunichte macht.
Das gesagt, unser Playbook muss:
- die NTP-Peers hinzufügen, die in der Liste vorhanden sind, aber nicht in der Gerätekonfiguration vorhanden sind;
- die NTP-Peers entfernen, die in der Gerätekonfiguration vorhanden sind, aber nicht in der Liste.
Der erste Teil ist äußerst einfach:
- cisco.ios.ios_config:
lines:
- "ntp peer {{ item }}"
replace: line
save_when: never
with_items: "{{ ntp_peers }}"
Der erste Teil ist äußerst einfach:
replace
: Nimmtline
(Standard) oderblock
als Wert und definiert, wie die Konfiguration angewendet wird. Mit line werden einzelne Befehle, die nicht in der Konfiguration vorhanden sind, gesendet, mitblock
werden alle Befehle gesendet, auch wenn einer von der Konfiguration abweicht.save_when
: Definiert, wann die Konfiguration gespeichert wird, und die Werte können sein:always
,never
(Standard),modified
,changed
.
Nun müssen wir die nicht mehr erforderlichen NTP-Peers entfernen. Dazu müssen wir zunächst alle konfigurierten NTP-Server extrahieren:
- ansible.builtin.set_fact:
current_ntp_peers: "{{ running_config | regex_findall('^ntp peer .*$', multiline=True) | regex_replace('ntp peer ', '')}}"
Schließlich können wir die nicht benötigten NTP-Server entfernen, d. h. diejenigen, die konfiguriert sind, aber nicht in der ntp_peers
-Liste vorhanden sind:
- cisco.ios.ios_config:
lines:
- "no ntp peer {{ item }}"
running_config: "{{ running_config }}"
replace: line
save_when: never
with_items: "{{ current_ntp_peers }}"
when: item not in ntp_peers
Dieser Ansatz gibt uns viel Freiheit, ist aber gleichzeitig komplex in der Verwaltung von verschachtelten Blockkonfigurationen (wie Routing).
Konfiguration speichern
An diesem Punkt haben die Geräte die gewünschte Konfiguration, aber sie wird nicht gespeichert. Dazu haben wir drei Ansätze:
handler
: Der es uns ermöglicht, eine Aufgabe auszuführen, wenn sie denchanged
-Zustand verursacht. Diese Methode ist die einfachste, hat jedoch ein Problem: Wenn das Playbook stoppt, besteht keine Garantie, dass die nächste Ausführung immer noch einenchanged
-Zustand meldet und folglich diehandler
-Aufgabe auslöst.- Die Konfiguration am Ende des Playbooks immer speichern: Diese Methode ist die einfachste, jedoch wird bei jeder Ausführung die Konfiguration gespeichert, und wir haben nie die Möglichkeit festzustellen, wann die Routerkonfiguration tatsächlich gespeichert werden muss.
- Überprüfen, ob sich die
running
- undstartup
-Konfigurationen unterscheiden und in diesem Fall tatsächlich die Konfiguration speichern.
Lassen Sie uns zunächst sehen, wie die Konfiguration gespeichert wird:
- cisco.ios.ios_command:
commands: write memory
Wir haben write memory
anstelle des modernen copy running-config startup-config
verwendet, weil ersterer keine Bestätigung erfordert, während letzterer dies tut: Die Verwaltung der Bestätigung wäre eine unnötige Komplexität.
Diese Aufgabe kann innerhalb der post_tasks
konfiguriert oder als handler
von allen Aufgaben aufgerufen werden, die möglicherweise die Konfiguration ändern.
Die Aufgaben werden wie folgt:
- cisco.ios.ios_config:
lines:
- "ntp peer {{ item }}"
replace: line
save_when: never
with_items: "{{ ntp_peers }}"
notify: SAVING CONFIGURATION
Während die Aufgabe zum Speichern der Konfiguration als handler
konfiguriert sein muss, idealerweise in der Datei handlers/main.yml.
Leistung optimieren
Wir müssen jetzt die Konfigurationsoptimierung vornehmen und sie nur durchführen, wenn dies erforderlich ist. Prozedural bedeutet dies, die startup-config
mit der running-config
zu vergleichen und bei Unterschieden die Speicherung durchzuführen.
Lassen Sie uns schrittweise vorgehen. Zunächst müssen wir die aktualisierte startup-config
und running-config
lesen:
- cisco.ios.ios_command:
commands: show startup-config
register: show_startup_config_output
- cisco.ios.ios_command:
commands: show running-config
register: show_running_config_output
Wir müssen dann überprüfen, ob sich die beiden Konfigurationen unterscheiden, unter Berücksichtigung einiger Zeilen.
- ansible.utils.fact_diff:
before: "{{ show_startup_config_output.stdout[0] }}"
after: "{{ show_running_config_output.stdout[0] }}"
plugin:
vars:
skip_lines:
- "^Current configuration.*"
- "^Building configuration.*"
register: config_differ
Wir können dann die Speicherung der Konfiguration vom Ergebnis der vorherigen Aufgabe abhängig machen, indem wir config_differ.changed
verwenden.
Eine Vorsichtsmaßnahme: Der Speichervorgang sollte nur erfolgen, wenn das Playbook nicht im Überprüfungsmodus (-C
) ausgeführt wird. Schließlich müssen wir daran denken, dass die Speicheraufgabe auch ausgeführt werden sollte, wenn das Playbook nur einige tags
enthält.
Schlussfolgerungen
Trotz allem ist es ratsam, spezialisierte Module zu verwenden, wenn sie fehlerfrei sind und alle Funktionen, die wir benötigen, implementieren. Dies gilt auch, wenn Leistungsprobleme auftreten. In meiner Erfahrung wird es jedoch oft notwendig sein, das generische Konfigurationsmodul zu verwenden, um spezifische Details zu konfigurieren, die nicht vorhergesehen sind.