Generació i ompliment automàtic d'elements de configuració de dispositius de xarxa mitjançant Nornir

Generació i ompliment automàtic d'elements de configuració de dispositius de xarxa mitjançant Nornir

Hola Habr!

Fa poc va aparèixer un article aquí Mikrotik i Linux. Rutina i automatització on es va resoldre un problema similar amb mitjans fòssils. I encara que la tasca és completament típica, no hi ha res semblant a Habré. M'atreveixo a oferir la meva bicicleta a la respectada comunitat informàtica.

Aquesta no és la primera bicicleta per a aquesta tasca. La primera opció es va implementar fa uns quants anys ansible versió 1.x.x. La bicicleta s'utilitzava poc i, per tant, s'oxidava constantment. En el sentit que la tasca en si no sorgeix tan sovint com s'actualitzen les versions ansible. I cada vegada que necessites conduir, cau la cadena o cau la roda. Tanmateix, la primera part, la generació de configuracions, sempre funciona molt clarament, afortunadament jinja2 El motor està establert des de fa temps. Però la segona part, desplegant les configuracions, sol portar sorpreses. I com que he de desplegar la configuració de forma remota a mig centenar de dispositius, alguns dels quals es troben a milers de quilòmetres de distància, utilitzar aquesta eina era una mica avorrit.

Aquí he d'admetre que la meva incertesa molt probablement rau en la meva falta de familiaritat ansibleque en les seves mancances. I aquest, per cert, és un punt important. ansible és una àrea de coneixement completament separada, amb la seva pròpia DSL (Domain Specific Language), que s'ha de mantenir a un nivell de confiança. Bé, aquell moment que ansible S'està desenvolupant amb força rapidesa i sense tenir en compte especial la compatibilitat amb versions anteriors, no afegeix confiança.

Per tant, no fa gaire es va implementar una segona versió de la bicicleta. Aquesta vegada pitó, o més aviat en un marc escrit pitó i per a pitó anomenat Nornir

Tan - Nornir és un microframe escrit en pitó i per a pitó i dissenyat per a l'automatització. El mateix que en el cas amb ansible, per resoldre els problemes aquí, es requereix una preparació de dades competent, és a dir. inventari d'amfitrions i els seus paràmetres, però els scripts no s'escriuen en un DSL separat, sinó en el mateix p[i|i]ton no molt antic, però molt bo.

Vegem què és utilitzant l'exemple en directe següent.

Tinc una xarxa d'oficines amb diverses desenes d'oficines a tot el país. Cada oficina té un encaminador WAN que finalitza diversos canals de comunicació de diferents operadors. El protocol d'encaminament és BGP. Els encaminadors WAN vénen de dos tipus: Cisco ISG o Juniper SRX.

Ara la tasca: heu de configurar una subxarxa dedicada per a la vigilància de vídeo en un port independent de tots els encaminadors WAN de la xarxa de sucursals - anunciar aquesta subxarxa en BGP - configurar el límit de velocitat del port dedicat.

En primer lloc, hem de preparar un parell de plantilles, a partir de les quals es generaran configuracions per separat per a Cisco i Juniper. També cal preparar dades per a cada punt i paràmetres de connexió, és a dir. recollir el mateix inventari

Plantilla preparada per a 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

Plantilla per a Juniper:

$ cat templates/junos/base.j2 
set interfaces {{ host.task_data.ifname }} unit 0 description "Video surveillance"
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

Les plantilles, per descomptat, no surten de la nada. Es tracta bàsicament de diferències entre les configuracions de treball que eren i eren després de resoldre la tasca en dos encaminadors específics de models diferents.

De les nostres plantilles veiem que per resoldre el problema, només necessitem dos paràmetres per a Juniper i 3 paràmetres per a Cisco. aquí estan:

  • ifname
  • ipsufix
  • ASN

Ara hem de configurar aquests paràmetres per a cada dispositiu, és a dir. fer el mateix inventari.

Per inventari Seguirem estrictament la documentació Inicialització de Nornir

és a dir, creem el mateix esquelet de fitxers:

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

El fitxer config.yaml és el fitxer de configuració estàndard de 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"

Indicarem els principals paràmetres al fitxer hosts.yaml, grup (en el meu cas són inicis de sessió/contrasenyes) a grups.yamli en defaults.yaml No indicarem res, però cal que introduïu tres punts menys allà, indicant que és així yaml però el fitxer està buit.

Aquest és el que sembla hosts.yaml:

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

I aquí teniu groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Això és el que va passar inventari per la nostra tasca. Durant la inicialització, els paràmetres dels fitxers d'inventari s'assignen al model d'objectes Element d'inventari.

A sota del spoiler hi ha un diagrama del model 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": "Username",
            "type": "string"
        },
        "password": {
            "title": "Password",
            "type": "string"
        },
        "platform": {
            "title": "Platform",
            "type": "string"
        },
        "groups": {
            "title": "Groups",
            "default": [],
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "data": {
            "title": "Data",
            "default": {},
            "type": "object"
        },
        "connection_options": {
            "title": "Connection_Options",
            "default": {},
            "type": "object",
            "additionalProperties": {
                "$ref": "#/definitions/ConnectionOptions"
            }
        }
    },
    "definitions": {
        "ConnectionOptions": {
            "title": "ConnectionOptions",
            "type": "object",
            "properties": {
                "hostname": {
                    "title": "Hostname",
                    "type": "string"
                },
                "port": {
                    "title": "Port",
                    "type": "integer"
                },
                "username": {
                    "title": "Username",
                    "type": "string"
                },
                "password": {
                    "title": "Password",
                    "type": "string"
                },
                "platform": {
                    "title": "Platform",
                    "type": "string"
                },
                "extras": {
                    "title": "Extras",
                    "type": "object"
                }
            }
        }
    }
}

Aquest model pot semblar una mica confús, sobretot al principi. Per esbrinar-ho, s'ha activat el mode interactiu ipython.

 $ ipython3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.1.1 -- An enhanced Interactive Python. Type '?' for help.

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'

I finalment, passem al guió en si. No tinc res de què estar especialment orgullós aquí. Acabo de prendre un exemple ja fet tutorial i el va utilitzar gairebé sense canvis. Aquest és l'aspecte de l'script de treball acabat:

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="Base Configuration",
                 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="Loading Configuration on the device",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

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

Preste atenció al paràmetre dry_run=Veritat inicialització d'objectes en línia nr.
Aquí el mateix que a ansible s'ha implementat una prova d'execució en la qual es fa una connexió amb l'encaminador, es prepara una nova configuració modificada, que després és validada pel dispositiu (però això no és segur; depèn del suport del dispositiu i de la implementació del controlador a NAPALM) , però la nova configuració no s'aplica directament. Per a l'ús de combat, heu d'eliminar el paràmetre córrer_seca o canviar-ne el valor a Fals.

Quan s'executa l'script, Nornir envia registres detallats a la consola.

A sota de l'spoiler hi ha la sortida d'un combat en dos encaminadors de prova:

config_and_deploy***************************************************************
* cisco-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : 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
  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
---- Loading Configuration on the device ** changed : True --------------------- 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
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Video surveillance"
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
---- Loading Configuration on the device ** changed : True --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "Video surveillance";
+           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;
+              }
+          }
+      }
+  }
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ocultar les contrasenyes a ansible_vault

Al principi de l'article vaig passar una mica per la borda ansible, però no és tan dolent. M'agraden molt volta com, que està dissenyat per ocultar informació sensible fora de la vista. I probablement molts s'han adonat que tenim tots els inicis de sessió/contrasenyes de tots els encaminadors de combat brillants en forma oberta en un fitxer gorups.yaml. No és bonic, és clar. Protegim aquestes dades amb volta.

Transferim els paràmetres de groups.yaml a creds.yaml i xifrem-lo amb AES256 amb una contrasenya de 20 dígits:

$ 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  
Encryption successful
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

És així de senzill. Queda per ensenyar el nostre Nornir-script per recuperar i aplicar aquestes dades.
Per fer-ho, en el nostre script després de la línia d'inicialització nr = InitNornir(fitxer_config=... afegiu el codi següent:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# enrich Inventory with the encrypted vault data
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))

# run tasks
...

Per descomptat, vault.passwd no s'ha de localitzar al costat de creds.yaml com en el meu exemple. Però està bé per jugar.

Això és tot per ara. Hi ha un parell d'articles més sobre Cisco + Zabbix, però no es tracta gaire d'automatització. I en un futur proper penso escriure sobre RESTCONF a Cisco.

Font: www.habr.com

Afegeix comentari