Generación y llenado automático de elementos de configuración de dispositivos de red utilizando Nornir

Generación y llenado automático de elementos de configuración de dispositivos de red utilizando Nornir

¡Hola, Habr!

Recientemente apareció un artículo aquí. Microtik y Linux. Rutina y automatización donde se resolvió un problema similar utilizando medios fósiles. Y aunque la tarea es completamente típica, en Habré no hay nada parecido. Me atrevo a ofrecer mi bicicleta a la respetada comunidad informática.

Esta no es la primera bicicleta que realiza tal tarea. La primera opción se implementó hace varios años en ansible versión 1.x.x. La bicicleta se utilizaba poco y, por tanto, se oxidaba constantemente. En el sentido de que la tarea en sí no surge con tanta frecuencia como se actualizan las versiones. ansible. Y cada vez que necesitas conducir, la cadena se cae o la rueda se cae. Sin embargo, la primera parte, generar configuraciones, siempre funciona muy claramente, afortunadamente. jinja2 El motor tiene una larga vida útil. Pero la segunda parte, la implementación de configuraciones, generalmente trajo sorpresas. Y como tengo que implementar la configuración de forma remota en medio centenar de dispositivos, algunos de los cuales se encuentran a miles de kilómetros de distancia, usar esta herramienta fue un poco aburrido.

Aquí debo admitir que mi incertidumbre probablemente radica en mi falta de familiaridad con ansibleque en sus defectos. Y este, dicho sea de paso, es un punto importante. ansible es un área de conocimiento completamente separada y propia con su propio DSL (lenguaje específico de dominio), que debe mantenerse en un nivel de confianza. Bueno, ese momento que ansible Se está desarrollando con bastante rapidez y, sin especial atención a la compatibilidad con versiones anteriores, no aporta confianza.

Por eso, no hace mucho se implementó una segunda versión de la bicicleta. esta vez en pitón, o más bien en un marco escrito en pitón y para pitón intitulado Nornir

Entonces - Nornir es un microframework escrito en pitón y para pitón y diseñado para la automatización. Lo mismo que en el caso de ansible, para resolver problemas aquí, se requiere una preparación de datos competente, es decir inventario de hosts y sus parámetros, pero los scripts no están escritos en un DSL separado, sino en el mismo p[i|i]ton no muy antiguo, pero sí muy bueno.

Veamos qué es usando el siguiente ejemplo en vivo.

Tengo una red de sucursales con varias decenas de oficinas en todo el país. Cada oficina cuenta con un enrutador WAN que termina varios canales de comunicación de diferentes operadores. El protocolo de enrutamiento es BGP. Los enrutadores WAN vienen en dos tipos: Cisco ISG o Juniper SRX.

Ahora la tarea: necesita configurar una subred dedicada para videovigilancia en un puerto separado en todos los enrutadores WAN de la red de la sucursal - anunciar esta subred en BGP - configurar el límite de velocidad del puerto dedicado.

Primero, debemos preparar un par de plantillas, a partir de las cuales se generarán configuraciones por separado para Cisco y Juniper. También es necesario preparar datos para cada punto y parámetros de conexión, es decir recoger el mismo inventario

Plantilla lista para 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 para 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

Las plantillas, por supuesto, no surgen de la nada. Estas son esencialmente diferencias entre las configuraciones de trabajo que había y que había después de resolver la tarea en dos enrutadores específicos de diferentes modelos.

De nuestras plantillas vemos que para resolver el problema, solo necesitamos dos parámetros para Juniper y 3 parámetros para Cisco. aquí están:

  • si nombre
  • ipsuffix
  • como

Ahora necesitamos configurar estos parámetros para cada dispositivo, es decir. hacer la misma cosa inventario.

para inventario Seguiremos estrictamente la documentación. Inicializando Nornir

es decir, creemos el mismo esqueleto de archivo:

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

El archivo config.yaml es el archivo de configuración estándar 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"

Indicaremos los parámetros principales en el archivo. hosts.yaml, grupo (en mi caso estos son nombres de usuario/contraseñas) en grupos.yaml, Y en predeterminados.yaml No indicaremos nada, pero allí debe ingresar tres desventajas, lo que indica que es yaml Sin embargo, el archivo está vacío.

Así es como se ve 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

Y aquí está groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Esto es lo que pasó inventario para nuestra tarea. Durante la inicialización, los parámetros de los archivos de inventario se asignan al modelo de objetos. Elemento de inventario.

Debajo del spoiler hay un diagrama del modelo 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"
                }
            }
        }
    }
}

Este modelo puede parecer un poco confuso, especialmente al principio. Para resolverlo, el modo interactivo en 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'

Y finalmente, pasemos al guión en sí. No tengo nada de qué estar particularmente orgulloso aquí. Acabo de tomar un ejemplo ya hecho de tutorial y lo usé casi sin cambios. Así es como se ve el script de trabajo terminado:

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)

Presta atención al parámetro. dry_run=Verdadero inicialización de objetos en línea nr.
Aquí lo mismo que en ansible se ha implementado una ejecución de prueba en la que se realiza una conexión al enrutador, se prepara una nueva configuración modificada, que luego es validada por el dispositivo (pero esto no es seguro; depende del soporte del dispositivo y de la implementación del controlador en NAPALM) , pero la nueva configuración no se aplica directamente. Para uso en combate, debes eliminar el parámetro. carrera_seca o cambiar su valor a Falso.

Cuando se ejecuta el script, Nornir envía registros detallados a la consola.

Debajo del spoiler se encuentra el resultado de una ejecución de combate en dos enrutadores de prueba:

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 contraseñas en ansible_vault

Al principio del artículo me excedí un poco. ansible, pero no es tan malo. me gustan mucho Bóveda like, que está diseñado para ocultar información confidencial fuera de la vista. Y probablemente muchos habrán notado que tenemos todos los nombres de usuario/contraseñas de todos los enrutadores de combate brillando en forma abierta en un archivo. grupos.yaml. No es bonito, por supuesto. Protejamos estos datos con Bóveda.

Transfiramos los parámetros de groups.yaml a creds.yaml y cifrémoslo con AES256 con una contraseña de 20 dígitos:

$ 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

Es así de simple. Queda por enseñar a nuestros Nornir-script para recuperar y aplicar estos datos.
Para hacer esto, en nuestro script después de la línea de inicialización nr = InitNornir(config_file=… agregue el siguiente código:

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

Por supuesto, vault.passwd no debe ubicarse junto a creds.yaml como en mi ejemplo. Pero está bien para jugar.

Eso es todo por ahora. Próximamente habrá un par de artículos más sobre Cisco + Zabbix, pero no se trata ni un poco de automatización. Y en un futuro próximo planeo escribir sobre RESTCONF en Cisco.

Fuente: habr.com

Añadir un comentario