Automatische Generierung und Bereitstellung von Konfigurationselementen fĂŒr NetzwerkgerĂ€te mit Nornir.

Automatische Generierung und Bereitstellung von Konfigurationselementen fĂŒr NetzwerkgerĂ€te mit Nornir.

Hallo, Habr!

Vor kurzem erschien hier ein Artikel Mikrotik und Linux. Routine und Automatisierung in dem eine Àhnliche Aufgabe mit veralteten Mitteln gelöst wurde. Obwohl die Aufgabe völlig typischer Natur ist, findet sich dazu auf Habr nichts Vergleichbares. Ich wage es, der geschÀtzten IT-Community mein eigenes Rad zu prÀsentieren.

Das ist nicht das erste Rad fĂŒr eine solche Aufgabe. Die erste Variante wurde vor einigen Jahren noch auf ansible Version 1.x.x umgesetzt. Dieses Rad wurde selten verwendet und fing daher stĂ€ndig an zu rosten. Das bedeutet, dass die eigentliche Aufgabe nicht so oft auftritt, wie die Versionen aktualisiert werden ansible. Und jedes Mal, wenn man fahren möchte, fĂ€llt die Kette ab oder das Rad löst sich. Die erste Phase, die Generierung der Konfigurationen, funktioniert jedoch stets sehr prĂ€zise, zum GlĂŒck jinja2 ist der Motor schon lange etabliert. Allerdings machte die zweite Phase—das Ausrollen der Konfigurationen—in der Regel Überraschungen. Da ich diese Konfigurationen aus der Ferne auf mehreren Dutzend GerĂ€ten ausrollen muss, von denen einige tausende Kilometer entfernt sind, war es etwas nervenaufreibend, dieses Werkzeug zu benutzen.

Hier muss ich zugeben, dass meine Unsicherheit eher in meinem unzureichenden Wissen ĂŒber ansible, als in seinen Nachteilen. Und das ist, by the way, ein wichtiger Punkt. ansible — das ist ein völlig eigenes Wissensgebiet mit einer eigenen DSL (Domain Specific Language), die auf sicherem Niveau gehalten werden muss. Und der Punkt, dass ansible sich recht schnell entwickelt, und zwar ohne besondere RĂŒcksicht auf die AbwĂ€rtskompatibilitĂ€t, trĂ€gt nicht zur Sicherheit bei.

Deshalb wurde nicht so lange her eine zweite Variante des Fahrrads realisiert. Dieses Mal auf Python, genauer gesagt auf einem Framework, das in Python und fĂŒr Python mit dem Namen Nornir

Also — Nornir ist ein Mikro-Framework, das in Python und fĂŒr Python geschrieben wurde und fĂŒr die Automatisierung gedacht ist. So wie auch im Fall von ansible, erfordert die Lösung von Aufgaben hier eine sorgfĂ€ltige Datenaufbereitung, das heißt, eine Inventarisierung der Hosts und ihrer Parameter, jedoch werden die Skripte nicht in einer separaten DSL geschrieben, sondern alles in der nicht sehr alten, aber sehr soliden Sprache [p|y]thon.

Lassen Sie uns betrachten, was es ist, anhand des nÀchsten Live-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 sind in zwei Typen unterteilt: Cisco ISG oder Juniper SRX.

Jetzt besteht die Aufgabe darin, auf allen WAN-Routern des Filialnetzes ein separates Subnetz fĂŒr die VideoĂŒberwachung ĂŒber einen speziellen Port zu konfigurieren — dieses Subnetz in BGP zu vertreiben — und die BandbreitenbeschrĂ€nkung des speziellen Ports einzurichten.

ZunĂ€chst 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 zu jedem Punkt und die Anschlussparameter sammeln, also das entsprechende Inventar erstellen.

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 any

Vorlage 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 limiter

Vorlagen kommen natĂŒrlich nicht aus dem Nichts. Es handelt sich im Grunde um die Unterschiede zwischen den Arbeitskonfigurationen vor und nach der Lösung der gestellten Aufgabe auf zwei bestimmten Routern verschiedener 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. Hier sind sie:

  • ifname
  • ipsuffix
  • asn

Jetzt mĂŒssen wir diese Parameter fĂŒr jedes GerĂ€t festlegen, d.h. das machen, inventory.

FĂŒr inventory wir werden strikt der Dokumentation folgen Nornir initialisieren

d.h. wir werden ein Àhnliches Dateiskellet erstellen:

.
├── config.yaml
├── inventory
│   ├── defaults.yaml
│   ├── groups.yaml
│   └── hosts.yaml

Die Datei config.yaml ist die Standardkonfigurationsdatei fĂŒr 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, Gruppendaten (in meinem Fall Benutzer/Passwörter) in groups.yaml, sondern in defaults.yaml werden wir nichts angeben, aber dort mĂŒssen drei Striche eingetragen werden — die darauf hinweisen, dass es sich um eine yaml Datei handelt, auch wenn sie 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: 65111

Und so sieht groups.yaml aus:

---
cisco:
    platform: ios
    username: admin1
    password: cisco1

juniper:
    platform: junos
    username: admin2
    password: juniper2

Das ist das Ergebnis inventory fĂŒr unsere Aufgabe. Bei der Initialisierung werden die Parameter aus den Inventardateien auf das Objektmodell InventoryElement.

gemappt. Unter dem Spoiler ist das Modellschema von 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 wirken. Der interaktive Modus in hilft sehr, um sich damit zurechtzufinden. ipython.

 $ ipython3
Python 3.6.9 (Standard, 7. Nov 2019, 10:44:02)
Geben Sie 'copyright', 'credits' oder 'license' ein, um weitere Informationen zu erhalten.
IPython 7.1.1 – Ein verbessertes interaktives Python. Geben Sie '?' fĂŒr Hilfe ein.

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 selbst. Ich habe hier nichts Besonderes zu zeigen. Ich habe einfach ein fertiges Beispiel aus dem Tutorial genommen und nahezu unverĂ€ndert 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):
    # Transform inventory data to configuration via a template file
    r = task.run(task=text.template_file,
                 name="Basis Konfiguration",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Save the compiled configuration into a host variable
    task.host["config"] = r.result

    # Save the compiled configuration into a file
    with open(f"configs/{task.host.hostname}", "w") as f:
        f.write(r.result)

    # Deploy that configuration to the device using NAPALM
    task.run(task=networking.napalm_configure,
             name="Konfiguration auf das GerÀt laden",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, Daumen drĂŒcken und erneut ausfĂŒhren

# run tasks
result = nr.run(task=config_and_deploy)
print_result(result)

Beachten Sie den Parameter dry_run=True bei der Initialisierung des Objekts nr.
Hier ist es Ă€hnlich wie in der ansible es wird ein Testlauf durchgefĂŒhrt, 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; es hĂ€ngt von der UnterstĂŒtzung des GerĂ€ts und der Implementierung des Treibers in NAPALM ab), aber die Anwendung der neuen Konfiguration erfolgt nicht. FĂŒr den produktiven Einsatz muss der Parameter dry_run entfernt oder sein Wert geĂ€ndert werden auf False.

Beim AusfĂŒhren des Nornir-Szenarios werden detaillierte Protokolle in die Konsole ausgegeben.

Unter dem Spoiler die Ausgabe des Testlaufs auf zwei Test-Routern:

config_and_deploy***************************************************************
* cisco-test ** geÀndert : Wahr *******************************************
vvvv config_and_deploy ** geÀndert : Wahr vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis-Konfiguration ** geÀndert : Wahr ------------------------------------- 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
  description VIDEOSURV
  ip address 10.10.222.254 255.255.255.0
  service-policy input VIDEO_SURV

router bgp 65001
  network 10.10.222.0 mask 255.255.255.0

access-list 11 permit 10.10.222.0 0.0.0.255
access-list 111 permit ip 10.10.222.0 0.0.0.255 any
---- Konfiguration auf dem GerÀt laden ** geÀndert : Wahr --------------------- INFO
+class-map match-all VIDEO_SURV
+ match access-group 111
+policy-map VIDEO_SURV
+ class VIDEO_SURV
+interface GigabitEthernet0/1/1
+  description VIDEOSURV
+  ip address 10.10.222.254 255.255.255.0
+  service-policy input VIDEO_SURV
+router bgp 65001
+  network 10.10.222.0 mask 255.255.255.0
+access-list 11 permit 10.10.222.0 0.0.0.255
+access-list 111 permit ip 10.10.222.0 0.0.0.255 any
^^^^ ENDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** geÀndert : Wahr *******************************************
vvvv config_and_deploy ** geÀndert : Wahr vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis-Konfiguration ** geÀndert : Wahr ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "VideoĂŒberwachung"
set interfaces fe-0/0/2 unit 0 family inet filter input limit-in
set interfaces fe-0/0/2 unit 0 family inet address 10.10.111.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.111.0/24 exact
set security zones security-zone WAN interfaces fe-0/0/2
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 limiter
---- Konfiguration auf dem GerÀt laden ** geÀndert : Wahr --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "VideoĂŒberwachung";
+           family inet {
+               filter {
+                   input limit-in;
+               }
+               address 10.10.111.254/24;
+           }
+       }
+   }
[edit]
+  policy-options {
+      policy-statement export2bgp {
+          term 1 {
+              from {
+                  route-filter 10.10.111.0/24 exact;
+              }
+          }
+      }
+  }
[edit security zones]
     security-zone test-vpn { ... }
+    security-zone WAN {
+        interfaces {
+            fe-0/0/2.0;
+        }
+    }
[edit]
+  firewall {
+      policer policer-1m {
+          if-exceeding {
+              bandwidth-limit 1m;
+              burst-size-limit 187k;
+          }
+          then discard;
+      }
+      policer policer-1.5m {
+          if-exceeding {
+              bandwidth-limit 1500000;
+              burst-size-limit 280k;
+          }
+          then discard;
+      }
+      filter limit-in {
+          term 1 {
+              then {
+                  policer policer-1.5m;
+                  count limiter;
+              }
+          }
+      }
+  }
^^^^ ENDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Verstecken von Passwörtern in ansible_vault

Zu Beginn des Artikels habe ich etwas angestoßen auf ansible, aber so schlimm ist es nicht. Mir gefĂ€llt sehr, dass vault dazu gedacht ist, sensible Informationen aus den Augen zu halten. Viele haben wahrscheinlich bemerkt, dass unsere Anmeldedaten fĂŒr alle produktiven Router offen in der Datei gorups.yamlstehen. Das ist natĂŒrlich unschön. Lassen Sie uns diese Daten mit Hilfe von vault.

Wir verschieben die Parameter von groups.yaml nach creds.yaml und verschlĂŒsseln diese 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
31393434653165613432326636616636383665316465623036376631313162646435

So einfach ist das. Jetzt mĂŒssen wir unser Nornir-Skript beibringen, diese Daten abzurufen und anzuwenden.
DafĂŒr fĂŒgen wir in unserem Skript nach der Initialisierungszeile ein: nr = InitNornir(config_file=
 den folgenden Code hinzu:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # setze dry_run=False, gedrĂŒckt die Daumen und erneut ausfĂŒhren

# 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 zum Experimentieren reicht das aus.

Das ist vorerst alles. Es kommen noch ein paar Artikel ĂŒber Cisco + Zabbix, aber das ist ein bisschen außerhalb des Automatisierungsthemas. In naher Zukunft plane ich, ĂŒber RESTCONF in Cisco zu schreiben.

Quelle: habr.com

Erwerben Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster