
Hola Habr!
Recientemente, un artículo de Mikrotik y Linux se deslizó aquí . Rutina y automatización donde se resolvió un problema similar por medios fósiles. Y aunque la tarea es completamente típica, no se encuentra nada similar en Habré al respecto. Me atrevo a ofrecer mi bicicleta a la comunidad de TI de buena reputación.
Esta no es la primera bicicleta para esta tarea. La primera opción se implementó hace varios años en la versión ansible 1.x.x. La bicicleta rara vez se usaba y, por lo tanto, se oxidaba constantemente. En el sentido de que la tarea en sí no ocurre con tanta frecuencia como las versiones actualizadas de ansible . Y cada vez que necesite ir, la cadena se caerá, la rueda se caerá. Sin embargo, la primera parte, la generación de configuraciones, siempre funciona muy claramente, ya que el motor se ha establecido durante mucho tiempo para jinja2 . Pero la segunda parte: el despliegue de configuraciones, como regla, trajo sorpresas. Y dado que tengo que desplegar la configuración de forma remota en el piso de cientos de dispositivos, algunos de los cuales están a miles de kilómetros de distancia, fue un poco molesto usar esta herramienta.
Aquí debo admitir que mi inseguridad, más bien, radica en la falta de familiaridad conmigo que en sus deficiencias. Y esto, por cierto, es un punto importante. ansible es un dominio de conocimiento completamente separado y propio con su propio DSL (lenguaje específico de dominio), que debe mantenerse a un nivel confiable. Bueno, el momento en que ansible se está desarrollando bastante rápido, y sin especial atención a la compatibilidad con versiones anteriores, no agrega confianza.
Por lo tanto, no hace mucho tiempo, se implementó la segunda versión de la bicicleta. Esta vez en python , o mejor dicho, en un marco escrito en python y para python llamado Nornir
Entonces, Nornir es un microframework escrito en python y para python y destinado a la automatización. Como en el caso de ansible , para resolver problemas, se requiere una preparación de datos competente aquí, es decir inventario de hosts y sus parámetros, pero los scripts no están escritos en un DSL separado, sino todos en el mismo tono no muy antiguo, pero muy bueno [y | ah].
Veamos qué es en el siguiente ejemplo vivo.
Tengo una red de sucursales con varias docenas de oficinas en todo el país. En cada oficina, hay un enrutador WAN que termina varios canales de comunicación de diferentes operadores. El protocolo de enrutamiento es BGP. Hay dos tipos de enrutadores WAN: Cisco ISG o Juniper SRX.
Ahora, la tarea: es necesario configurar una subred dedicada para videovigilancia en un puerto separado en todos los enrutadores WAN de la red de sucursal - anuncie esta subred en BGP - configure el límite de velocidad para el puerto dedicado.
Primero, necesitamos preparar un par de plantillas, basadas en qué configuraciones se generarán por separado para Cisco y Juniper. Y también es necesario preparar datos para cada punto y parámetros de conexión, es decir para recoger ese 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 enebro:
$ 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 se toman del techo. Esto es esencialmente la diferencia entre las configuraciones de trabajo: se convirtió después de resolver la tarea en dos enrutadores específicos de diferentes modelos.
De nuestras plantillas, vemos que para resolver el problema, dos parámetros para Juniper y 3 parámetros para Cisco son suficientes. aqui estan:
Ahora necesitamos establecer estos parámetros para cada dispositivo, es decir hacer el mismo inventario
Para el inventario, seguiremos claramente la documentación de Inicialización de Nornir
es decir, crea el mismo esqueleto de archivo:
. ├── config.yaml ├── inventory │ ├── defaults.yaml │ ├── groups.yaml │ └── hosts.yaml
Archivo config.yaml: archivo de configuración nornir estándar
$ 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"
Especificaremos los parámetros principales en el archivo hosts.yaml , grupo (en mi caso, nombres de usuario / contraseñas) en groups.yaml , y no especificaremos nada en defaults.yaml , pero debe haber tres desventajas para indicar que se trata de un archivo yaml aunque vacio
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
Aquí está el inventario para nuestra tarea. Durante la inicialización, los parámetros de los archivos de inventario se asignan al modelo de objetos InventoryElement .
Debajo del spoiler, el 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. El modo interactivo en ipython ayuda mucho a resolverlo.
$ 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'
Bueno, finalmente, pasemos al guión mismo. No hay nada especial de lo que pueda estar orgulloso. Acabo de tomar el ejemplo final del 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):
Tenga en cuenta el parámetro dry_run = True en la línea de inicialización del objeto nr .
Aquí, al igual que en ansible , se implementa una ejecución de prueba en la que se realiza la conexión al enrutador, se prepara una nueva configuración modificada, que luego es validada por el dispositivo (pero esto no es exacto; depende del soporte del dispositivo y la implementación del controlador en NAPALM), pero la nueva configuración no se aplica directamente . Para el uso en combate, debe eliminar el parámetro dry_run o cambiar su valor a False .
Cuando se ejecuta el script, Nornir muestra registros detallados en la consola.
Bajo el spoiler, la conclusión del combate se ejecuta 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 comienzo del artículo, me encontré con Ansible un poco , pero no todo es tan malo allí. Realmente me gusta su bóveda , que está diseñada para ocultar información confidencial fuera de la vista. Y probablemente muchos notaron que tenemos todos los nombres de usuario / contraseñas para todos los enrutadores de batalla que brillan en forma abierta en el archivo groups.yaml . Es feo, por supuesto. Protejamos estos datos con bóveda .
Transferimos los parámetros de groups.yaml a creds.yaml y lo ciframos 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
Tan simple Queda por enseñar a nuestro script Nornir a obtener y usar 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)
Por supuesto, vault.passwd no debería estar al lado de creds.yaml como en mi ejemplo. Pero jugar lo hará.
Eso es todo por ahora. Llegan un par de artículos sobre Cisco + Zabbix, pero no se trata solo de automatización. Y en un futuro próximo planeo escribir sobre RESTCONF en Cisco.