
¡Hola, Habr!
Recientemente apareció un artículo aquí donde se resolvió una tarea similar con medios obsoletos. Y aunque la tarea es completamente típica, no se encuentra nada similar sobre ella en Habr. Me atreveré a proponer a la respetada comunidad de TI mi propia solución.
No es la primera solución para este tipo de tarea. La primera versión se implementó hace varios años en ansible la versión 1.x.x. La solución fue utilizada rara vez y, por lo tanto, se oxidadaba constantemente. En el sentido de que la tarea misma no surge tan frecuentemente como se actualizan las versiones ansible. Y cada vez que es necesario llevar a cabo la tarea, ya sea la cadena se cae o la rueda se desprende. Sin embargo, la primera parte, la generación de configuraciones, siempre funciona de manera muy precisa, gracias a que jinja2 el motor está bien establecido desde hace tiempo. Pero la segunda parte, la implementación de configuraciones, usualmente presentaba sorpresas. Y dado que tengo que implementar configuraciones de forma remota en unas cincuenta dispositivos, algunos de los cuales se encuentran a miles de kilómetros, usar esta herramienta era un poco incómodo.
Aquí debo reconocer que mi falta de confianza en realidad proviene de mi insuficiente familiaridad con ansible, más que de sus deficiencias. Y ese es, por cierto, un punto importante. ansible — es un área de conocimiento totalmente separada, con su propio DSL (Lenguaje Específico de Dominio) que debe mantenerse a un nivel seguro. Y el hecho de que ansible se desarrolla bastante rápido, sin prestar especial atención a la compatibilidad hacia atrás, no añade confianza.
Por lo tanto, no hace mucho se implementó la segunda versión de la solución. Esta vez en python, y más precisamente en un marco desarrollado en python y para python llamada
Así que — Nornir es un micro marco, desarrollado en python y para python y destinado a la automatización. Al igual que en el caso de ansible, para resolver las tareas aquí se requiere una buena preparación de datos, es decir, un inventario de hosts y sus parámetros, pero los guiones se escriben no en un DSL especial, sino todo en el mismo Python, que no es muy antiguo, pero es muy bueno.
Veamos qué es esto con un siguiente ejemplo en vivo.
Tengo una red de filiales con varias decenas 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. Los enrutadores WAN pueden ser de dos tipos: Cisco ISG o Juniper SRX.
Ahora la tarea es: necesitar configurar en todos los enrutadores WAN de la red de sucursales una subred dedicad para videovigilancia en un puerto separado, anunciar esta subred en BGP y configurar la limitación de velocidad del puerto dedicado.
Primero necesitamos preparar un par de plantillas, a partir de las cuales se generarán las configuraciones por separado para Cisco y Juniper. También debemos preparar los datos para cada punto y los parámetros de conexión, es decir, recopilar el 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 anyPlantilla para Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Videovigilancia"
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 limiterLas plantillas, por supuesto, no se toman de la nada. Básicamente son diffs entre las configuraciones de trabajo que existían y las que se tienen después de resolver la tarea planteada en dos enrutadores concretos de diferentes modelos.
De nuestras plantillas, vemos que para resolver la tarea necesitamos dos parámetros para Juniper y tres parámetros para Cisco. Aquí están:
- ifname
- ipsuffix
- asn
Ahora necesitamos establecer estos parámetros para cada dispositivo, es decir, hacer lo mismo. Podemos intentar ejecutar ansible con el archivo de inventario:.
Para Podemos intentar ejecutar ansible con el archivo de inventario: seguiremos estrictamente la documentación
es decir, crearemos la misma estructura de archivos:
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlEl 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"Los parámetros principales los indicaremos en el archivo hosts.yaml, grupos (en mi caso son los nombres de usuario/contraseñas) en groups.yaml, mientras que en defaults.yaml no indicaremos nada, pero allí es necesario escribir tres guiones — que indican que esto es yaml el archivo aunque 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: 65111Y así es groups.yaml:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2Así ha quedado Podemos intentar ejecutar ansible con el archivo de inventario: para nuestra tarea. Al inicializar, los parámetros de los archivos de inventario se asignan al modelo de objeto InventoryElement.
Bajo el spoiler está el esquema 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 entenderlo, el modo interactivo ayuda mucho. 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 -- Un Python Interactivo mejorado. Escriba '?' para ayuda.
En [1]: from nornir import InitNornir
En [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)
En [3]: nr.inventory.hosts
Out[3]:
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}
En [4]: nr.inventory.hosts['srx-test'].data
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}
En [5]: nr.inventory.hosts['srx-test']['task_data']
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}
En [6]: nr.inventory.hosts['srx-test'].platform
Out[6]: 'junos'
Y finalmente llegamos al script en sí. No tengo mucho de qué presumir aquí. Simplemente tomé un ejemplo ya preparado de y lo utilicé casi sin modificaciones. Así es como se ve el script funcional completo:
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):
# Transformar los datos de inventario a configuración a través de un archivo de plantilla
r = task.run(task=text.template_file,
name="Configuración Base",
template="base.j2",
path=f"templates/{task.host.platform}")
# Guardar la configuración compilada en una variable del host
task.host["config"] = r.result
# Guardar la configuración compilada en un archivo
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Desplegar esa configuración en el dispositivo usando NAPALM
task.run(task=networking.napalm_configure,
name="Cargando configuración en el dispositivo",
replace=False,
configuration=task.host["config"])
nr = InitNornir(config_file="config.yaml", dry_run=True) # establece dry_run=False, cruza los dedos y corre de nuevo
# ejecutar tareas
result = nr.run(task=config_and_deploy)
print_result(result)Presta atención al parámetro dry_run=True en la línea de inicialización del objeto nr.
Aquí, también se ha implementado una prueba donde se establece la conexión con el enrutador, se prepara una nueva configuración modificada, que luego es validada por el dispositivo (aunque esto no es seguro; depende del soporte del dispositivo y la implementación del controlador en NAPALM), pero no se aplica la nueva configuración directamente. Para un uso en producción, es necesario eliminar el parámetro ansible dry_run o cambiar su valor a Al ejecutar el script, Nornir proporciona registros detallados en la consola. False.
Bajo el spoiler está la salida de ejecución en un entorno de prueba en dos enrutadores de prueba:
Bajo el spoiler, se muestra el resultado de la ejecución de pruebas en dos enrutadores de prueba:
config_and_deploy***************************************************************
* cisco-test ** cambiado: True *******************************************
vvvv config_and_deploy ** cambiado: True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuración Base ** cambiado: 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
---- Cargando configuración en el dispositivo ** cambiado: 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
^^^^ FIN config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** cambiado: True *******************************************
vvvv config_and_deploy ** cambiado: True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuración Base ** cambiado: True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Videovigilancia"
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
---- Cargando Configuración en el dispositivo ** cambiado: True --------------------- INFO
[edit interfaces]
+ fe-0/0/2 {
+ unit 0 {
+ description "Videovigilancia";
+ 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;
+ }
+ }
+ }
+ }
^^^^ FIN config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Ocultamos contraseñas en ansible_vault
Al principio del artículo me referí a ansible, pero no es tan malo. Me gustan mucho vault que está diseñado para ocultar información sensible a la vista. Y probablemente muchos han notado que nuestras credenciales de acceso a todos los enrutadores de producción están expuestas en texto claro en un archivo gorups.yaml. Esto no es muy elegante, por supuesto. Protejamos estos datos usando vault.
Movemos los parámetros de groups.yaml a creds.yaml y lo ciframos con AES256 usando una contraseña de 20 caracteres:
$ 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
Cifrado exitoso
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435Así de fácil. Ahora solo falta enseñar a nuestro Nornir-script a extraer y aplicar estos datos.
Para ello, en nuestro script, después de la línea de inicialización nr = InitNornir(config_file=… agregamos el siguiente código:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # establece dry_run=False, cruza los dedos y vuelve a ejecutar
# enriquece el inventario con los datos cifrados del vault
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))
# ejecutar tareas
...Por supuesto, vault.passwd no debe estar junto a creds.yaml como en mi ejemplo. Pero está bien para jugar un poco.
Por ahora eso es todo. Se acercan un par de artículos sobre Cisco + Zabbix, pero eso es un poco distinto a la automatización. Y en un futuro cercano planeo escribir sobre RESTCONF en Cisco.
Fuente: habr.com
