
Hallo, Habra!
KĂŒrzlich erschien hier ein Artikel wo eine Ă€hnliche Aufgabe mit veralteten Mitteln gelöst wurde. Und obwohl die Aufgabe ganz typisch ist, findet sich auf Habr nichts Vergleichbares dazu. Ich wage es, der geschĂ€tzten IT-Community mein eigenes Rad zu prĂ€sentieren.
Dies ist nicht das erste Rad fĂŒr eine solche Aufgabe. Die erste Variante wurde vor einigen Jahren noch auf ansible Version 1.x.x implementiert. Das Rad wurde selten verwendet und rostete daher stĂ€ndig. In dem Sinne, dass die Aufgabe nicht so oft auftaucht, wie die Versionen aktualisiert werden. ansible. Und jedes Mal, wenn es nötig war, zu fahren, fiel die Kette ab oder das Rad fiel ab. Die erste Teil, die Generatorerstellung von Konfigurationen, arbeitet jedoch immer sehr prĂ€zise, zum GlĂŒck jinja2 ist der Motor schon seit langem bewĂ€hrt. Aber der zweite Teil â das Ausrollen von Konfigurationen, bescherte in der Regel Ăberraschungen. Und da ich die Konfigurationen aus der Ferne auf rund fĂŒnfzig GerĂ€ten ausrollen muss, von denen sich einige tausende von Kilometern entfernt befinden, war es ein wenig beĂ€ngstigend, dieses Werkzeug zu verwenden.
Hier muss ich zugeben, dass mein Zögern eher in meinem unzureichenden Wissen ĂŒber ansible, als in seinen MĂ€ngeln liegt. Und das ist ĂŒbrigens ein wichtiger Punkt. ansible â es ist ein ganz eigenes, separater Wissensbereich mit seiner eigenen DSL (Domain Specific Language), die auf einem soliden Niveau gehalten werden muss. Und der Punkt, dass ansible sich ziemlich schnell entwickelt, ohne besonders auf die RĂŒckwĂ€rtskompatibilitĂ€t zu achten, trĂ€gt nicht zur Sicherheit bei.
Deshalb wurde vor nicht allzu langer Zeit eine zweite Variante des Rades umgesetzt. Diesmal auf python, genauer gesagt auf einem Framework, das in python und fĂŒr python mit dem Namen
geschrieben wurde. Also â Nornir ist ein Mikro-Framework, das in python und fĂŒr python geschrieben wurde und fĂŒr die Automatisierung gedacht ist. Genau wie im Fall von ansible, erfordert die Lösung hier eine sorgfĂ€ltige Datenvorbereitung, d.h. eine Inventarisierung von Hosts und deren Parametern, wĂ€hrend die Szenarien nicht in einer separaten DSL, sondern in demselben nicht allzu alten, aber sehr netten Python geschrieben werden.
Lassen Sie uns sehen, was es ist, anhand des folgenden praktischen Beispiels.
Ich habe ein Filialnetz mit mehreren Dutzend BĂŒros im ganzen Land. In jedem BĂŒro gibt es einen WAN-Router, der mehrere KommunikationskanĂ€le von verschiedenen Anbietern terminiert. Das Routing-Protokoll ist BGP. WAN-Router gibt es in zwei Typen: Cisco ISG oder Juniper SRX.
Jetzt die Aufgabe: Es ist notwendig, auf allen WAN-Routern des Filialnetzwerks ein dediziertes Subnetz fĂŒr VideoĂŒberwachung an einem separaten Port zu konfigurieren â dieses Subnetz im BGP anzukĂŒndigen â die BandbreitenbeschrĂ€nkung des dedizierten Ports zu konfigurieren.
Zuerst mĂŒssen wir eine Reihe von Vorlagen vorbereiten, auf deren Basis die Konfigurationen separat fĂŒr Cisco und Juniper generiert werden. AuĂerdem mĂŒssen wir die Daten fĂŒr jeden Punkt und die Anschlussparameter zusammenstellen, d.h. das entsprechende Inventar sammeln.
Fertige Vorlage fĂŒr Cisco:
$ cat templates/ios/base.j2
class-map match-all VIDEO_SURV
match access-group 111
policy-map VIDEO_SURV
class VIDEO_SURV
police 1500000 conform-action transmit exceed-action drop
interface {{ host.task_data.ifname }}
description VIDEOSURV
ip address 10.10.{{ host.task_data.ipsuffix }}.254 255.255.255.0
service-policy input VIDEO_SURV
router bgp {{ host.task_data.asn }}
network 10.40.{{ host.task_data.ipsuffix }}.0 mask 255.255.255.0
access-list 11 permit 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255
access-list 111 permit ip 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255 anyVorlage fĂŒr Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "VideoĂŒberwachung"
set interfaces {{ host.task_data.ifname }} unit 0 family inet filter input limit-in
set interfaces {{ host.task_data.ifname }} unit 0 family inet address 10.10.{{ host.task_data.ipsuffix }}.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.{{ host.task_data.ipsuffix }}.0/24 exact
set security zones security-zone WAN interfaces {{ host.task_data.ifname }}
set firewall policer policer-1m if-exceeding bandwidth-limit 1m
set firewall policer policer-1m if-exceeding burst-size-limit 187k
set firewall policer policer-1m then discard
set firewall policer policer-1.5m if-exceeding bandwidth-limit 1500000
set firewall policer policer-1.5m if-exceeding burst-size-limit 280k
set firewall policer policer-1.5m then discard
set firewall filter limit-in term 1 then policer policer-1.5m
set firewall filter limit-in term 1 then count limiterDie Vorlagen stammen natĂŒrlich nicht aus der Luft. Es handelt sich im Wesentlichen um die Differenzen zwischen den Arbeitskonfigurationen vor und nach der Lösung der gestellten Aufgabe auf zwei bestimmten Routern unterschiedlicher Modelle.
Aus unseren Vorlagen sehen wir, dass wir fĂŒr die Lösung der Aufgabe zwei Parameter fĂŒr Juniper und drei Parameter fĂŒr Cisco benötigen. Diese sind:
- ifname
- ipsuffix
- asn
Jetzt mĂŒssen wir diese Parameter fĂŒr jedes GerĂ€t festlegen, d.h. das eben Genannte umsetzen. inventory.
FĂŒr inventory Wir werden genau der Dokumentation folgen.
Das heiĂt, wir erstellen ein Ă€hnliches Verzeichnislayout:
.
âââ config.yaml
âââ inventory
â âââ defaults.yaml
â âââ groups.yaml
â âââ hosts.yamlDie Datei config.yaml ist die Standard-Konfigurationsdatei von Nornir.
$ cat config.yaml
---
core:
num_workers: 10
inventory:
plugin: nornir.plugins.inventory.simple.SimpleInventory
options:
host_file: "inventory/hosts.yaml"
group_file: "inventory/groups.yaml"
defaults_file: "inventory/defaults.yaml"Die Hauptparameter geben wir in der Datei an. hosts.yaml, Gruppen (in meinem Fall sind das Logins/Passwörter) in groups.yaml, sondern in defaults.yaml wir werden nichts angeben, aber dort mĂŒssen drei Minuspunkte eingetragen werden â die darauf hinweisen, dass es sich um yaml die Datei zwar leer ist.
So sieht hosts.yaml aus:
---
srx-test:
hostname: srx-test
groups:
- juniper
data:
task_data:
ifname: fe-0/0/2
ipsuffix: 111
cisco-test:
hostname: cisco-test
groups:
- cisco
data:
task_data:
ifname: GigabitEthernet0/1/1
ipsuffix: 222
asn: 65111Und so sieht groups.yaml aus:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2So sieht es aus inventory fĂŒr unsere Aufgabe. Bei der Initialisierung werden die Parameter aus den Inventory-Dateien auf das Objektmodell abgebildet InventoryElement.
Unter dem Spoiler befindet sich das Schema des Modells InventoryElement
print(json.dumps(InventoryElement.schema(), indent=4))
{
"title": "InventoryElement",
"type": "object",
"properties": {
"hostname": {
"title": "Hostname",
"type": "string"
},
"port": {
"title": "Port",
"type": "integer"
},
"username": {
"title": "Benutzername",
"type": "string"
},
"password": {
"title": "Passwort",
"type": "string"
},
"platform": {
"title": "Plattform",
"type": "string"
},
"groups": {
"title": "Gruppen",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"data": {
"title": "Daten",
"default": {},
"type": "object"
},
"connection_options": {
"title": "Verbindungsoptionen",
"default": {},
"type": "object",
"additionalProperties": {
"$ref": "#\/definitions\/ConnectionOptions"
}
}
},
"definitions": {
"ConnectionOptions": {
"title": "Verbindungsoptionen",
"type": "object",
"properties": {
"hostname": {
"title": "Hostname",
"type": "string"
},
"port": {
"title": "Port",
"type": "integer"
},
"username": {
"title": "Benutzername",
"type": "string"
},
"password": {
"title": "Passwort",
"type": "string"
},
"platform": {
"title": "Plattform",
"type": "string"
},
"extras": {
"title": "Extras",
"type": "object"
}
}
}
}
}Dieses Modell kann anfangs etwas verwirrend erscheinen. Um die ZusammenhÀnge zu verstehen, hilft der interaktive Modus sehr. ipython.
$ ipython3
Python 3.6.9 (Standard, 7. Nov. 2019, 10:44:02)
Tippe 'copyright', 'credits' oder 'license' fĂŒr weitere Informationen
IPython 7.1.1 -- Ein erweitertes interaktives Python. Tippe '?' fĂŒr Hilfe.
In [1]: from nornir import InitNornir
In [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)
In [3]: nr.inventory.hosts
Out[3]:
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}
In [4]: nr.inventory.hosts['srx-test'].data
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}
In [5]: nr.inventory.hosts['srx-test']['task_data']
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}
In [6]: nr.inventory.hosts['srx-test'].platform
Out[6]: 'junos'
Und schlieĂlich kommen wir zum Skript. Ich habe hier nichts, worauf ich besonders stolz sein könnte. Ich habe einfach ein fertiges Beispiel aus genommen und fast ohne Ănderungen verwendet. So sieht das fertige funktionierende Skript aus:
from nornir import InitNornir
from nornir.plugins.tasks import networking, text
from nornir.plugins.functions.text import print_title, print_result
def config_and_deploy(task):
# Transformiere Inventardaten in Konfiguration ĂŒber eine Vorlagendatei
r = task.run(task=text.template_file,
name="Basis-Konfiguration",
template="base.j2",
path=f"templates/{task.host.platform}")
# Speichere die kompilierte Konfiguration in einer Host-Variablen
task.host["config"] = r.result
# Speichere die kompilierte Konfiguration in einer Datei
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Ăbertrage diese Konfiguration auf das GerĂ€t mit NAPALM
task.run(task=networking.napalm_configure,
name="Lade Konfiguration auf das GerÀt",
replace=False,
configuration=task.host["config"])
nr = InitNornir(config_file="config.yaml", dry_run=True) # setze dry_run=False, drĂŒcke die Daumen und fĂŒhren Sie erneut aus
# Aufgaben ausfĂŒhren
result = nr.run(task=config_and_deploy)
print_result(result)Bitte beachten Sie den Parameter dry_run=True in der Zeile zur Initialisierung des Objekts nr.
Hier wird ebenfalls ein Testlauf realisiert, bei dem eine Verbindung zum Router hergestellt wird, eine neue geĂ€nderte Konfiguration vorbereitet wird, die dann vom GerĂ€t validiert wird (aber das ist nicht sicher; hĂ€ngt von der UnterstĂŒtzung durch das GerĂ€t und der Implementierung des Treibers in NAPALM ab), aber die Anwendung der neuen Konfiguration findet nicht statt. FĂŒr den produktiven Einsatz muss der Parameter ansible dry_run entfernt oder seinen Wert auf geĂ€ndert werden. Falsch.
Beim AusfĂŒhren des Nornir-Skripts gibt es detaillierte Protokolle auf der Konsole.
Unter dem Spoiler die Ausgabe eines produktiven Durchlaufs auf zwei Test-Routern:
config_and_deploy***************************************************************
* cisco-test ** geÀndert: True *******************************************
vvvv config_and_deploy ** geÀndert: True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis Konfiguration ** geÀndert: True ------------------------------------- INFO
class-map match-all VIDEO_SURV
match access-group 111
policy-map VIDEO_SURV
class VIDEO_SURV
police 1500000 conform-action transmit exceed-action drop
interface GigabitEthernet0/1/1
Beschreibung VIDEOSURV
ip-Adresse 10.10.222.254 255.255.255.0
service-policy input VIDEO_SURV
router bgp 65001
Netzwerk 10.10.222.0 Maske 255.255.255.0
access-list 11 erlauben 10.10.222.0 0.0.0.255
access-list 111 erlauben ip 10.10.222.0 0.0.0.255 any
---- Konfiguration auf dem GerÀt laden ** geÀndert: True --------------------- INFO
+class-map match-all VIDEO_SURV
+ match access-group 111
+policy-map VIDEO_SURV
+ class VIDEO_SURV
+interface GigabitEthernet0/1/1
+ Beschreibung VIDEOSURV
+ ip-Adresse 10.10.222.254 255.255.255.0
+ service-policy input VIDEO_SURV
+router bgp 65001
+ Netzwerk 10.10.222.0 Maske 255.255.255.0
+access-list 11 erlauben 10.10.222.0 0.0.0.255
+access-list 111 erlauben ip 10.10.222.0 0.0.0.255 any
^^^^ ENDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** geÀndert: True *******************************************
vvvv config_and_deploy ** geÀndert: True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis Konfiguration ** geÀndert: True ------------------------------------- INFO
set interfaces fe-0/0/2 einheit 0 Beschreibung "VideoĂŒberwachung"
set interfaces fe-0/0/2 einheit 0 Familie inet Filter Eingabe limit-in
set interfaces fe-0/0/2 einheit 0 Familie inet Adresse 10.10.111.254/24
set policy-options policy-statement export2bgp term 1 von route-filter 10.10.111.0/24 genau
set Sicherheitszonen Sicherheitszone WAN Schnittstellen fe-0/0/2
set Firewall Policier policer-1m if-exceeding bandwidth-limit 1m
set Firewall Policier policer-1m if-exceeding burst-size-limit 187k
set Firewall Policier policer-1m dann verwerfen
set Firewall Policier policer-1.5m if-exceeding bandwidth-limit 1500000
set Firewall Policier policer-1.5m if-exceeding burst-size-limit 280k
set Firewall Policier policer-1.5m dann verwerfen
set Firewall Filter limit-in Termin 1 dann Policier policer-1.5m
set Firewall Filter limit-in Termin 1 dann zÀhlen Begrenzer
---- Konfiguration auf dem GerÀt laden ** geÀndert: True --------------------- INFO
[edit Schnittstellen]
+ fe-0/0/2 {
+ einheit 0 {
+ Beschreibung "VideoĂŒberwachung";
+ Familie inet {
+ Filter {
+ Eingabe limit-in;
+ }
+ Adresse 10.10.111.254/24;
+ }
+ }
+ }
[edit]
+ policy-options {
+ policy-statement export2bgp {
+ Termin 1 {
+ von {
+ route-filter 10.10.111.0/24 genau;
+ }
+ }
+ }
+ }
[edit Sicherheitszonen]
Sicherheitszone test-vpn { ... }
+ Sicherheitszone WAN {
+ Schnittstellen {
+ fe-0/0/2.0;
+ }
+ }
[edit]
+ Firewall {
+ Policier policer-1m {
+ if-exceeding {
+ bandwidth-limit 1m;
+ burst-size-limit 187k;
+ }
+ dann verwerfen;
+ }
+ Policier policer-1.5m {
+ if-exceeding {
+ bandwidth-limit 1500000;
+ burst-size-limit 280k;
+ }
+ dann verwerfen;
+ }
+ Filter limit-in {
+ Termin 1 {
+ dann {
+ Policier policer-1.5m;
+ zÀhlen Begrenzer;
+ }
+ }
+ }
+ }
^^^^ ENDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Wir verstecken Passwörter in ansible_vault
Zu Beginn des Artikels habe ich ein wenig auf ansible, aber so schlimm ist es dort nicht. Ich finde sie wirklich vault Es gefĂ€llt, dass es dazu dient, sensible Informationen auĂerhalb der Sicht zu verbergen. Und wahrscheinlich haben viele bemerkt, dass alle unsere Logins/Passwörter fĂŒr alle aktiven Router im Klartext in der Datei zu sehen sind. gorups.yaml. Das sieht natĂŒrlich nicht gut aus. Lassen Sie uns diese Daten mit Hilfe von vault.
Wir ĂŒbertragen die Parameter aus groups.yaml nach creds.yaml und verschlĂŒsseln es mit AES256 und einem 20-stelligen Passwort:
$ cd inventory
$ cat creds.yaml
---
cisco:
username: admin1
password: cisco1
juniper:
username: admin2
password: juniper2
$ pwgen 20 -N 1 > vault.passwd
ansible-vault encrypt creds.yaml --vault-password-file vault.passwd
VerschlĂŒsselung erfolgreich
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435So einfach ist das. Wir mĂŒssen nur noch unser Nornir-Skript lehren, diese Daten abzurufen und anzuwenden.
Dazu fĂŒgen wir in unserem Skript nach der Initialisierungszeile nr = InitNornir(config_file=⊠den folgenden Code hinzu:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # setze dry_run=False, drĂŒcke die Daumen und fĂŒhre erneut aus
# Inventar mit den verschlĂŒsselten Vault-Daten anreichern
from ansible_vault import Vault
vault_password_file="inventory/vault.passwd"
vault_file="inventory/creds.yaml"
with open(vault_password_file, "r") as fp:
password = fp.readline().strip()
vault = Vault(password)
vaultdata = vault.load(open(vault_file).read())
for a in nr.inventory.hosts.keys():
item = nr.inventory.hosts[a]
item.username = vaultdata[item.groups[0]]['username']
item.password = vaultdata[item.groups[0]]['password']
#print("hostname={}, username={}, password={}n".format(item.hostname, item.username, item.password))
# Aufgaben ausfĂŒhren
...NatĂŒrlich sollte vault.passwd nicht neben creds.yaml liegen, wie in meinem Beispiel. Aber um ein wenig zu experimentieren, genĂŒgt es.
Das ist vorerst alles. Es stehen noch ein paar Artikel ĂŒber Cisco + Zabbix auf dem Programm, aber das ist etwas weniger ĂŒber Automatisierung. In naher Zukunft plane ich, ĂŒber RESTCONF bei Cisco zu schreiben.
Quelle: habr.com
