Geração e preenchimento automático de elementos de configuração de dispositivos de rede usando Nornir

Geração e preenchimento automático de elementos de configuração de dispositivos de rede usando Nornir

Oi, Habr!

Recentemente apareceu um artigo aqui Mikrotik e Linux. Rotina e automação onde um problema semelhante foi resolvido usando meios fósseis. E embora a tarefa seja completamente típica, não há nada semelhante em Habré. Atrevo-me a oferecer minha bicicleta à respeitada comunidade de TI.

Esta não é a primeira bicicleta para tal tarefa. A primeira opção foi implementada há vários anos, em ansible versão 1.x.x. A bicicleta raramente era usada e, portanto, enferrujava constantemente. No sentido de que a tarefa em si não surge com tanta frequência quanto as versões são atualizadas ansible. E toda vez que você precisa dirigir, a corrente cai ou a roda cai. Porém, a primeira parte, gerar configurações, sempre funciona de forma muito clara, felizmente Jinja2 O motor está estabelecido há muito tempo. Mas a segunda parte – implementar configurações – geralmente trazia surpresas. E como tenho que implementar a configuração remotamente para meia centena de dispositivos, alguns dos quais estão localizados a milhares de quilômetros de distância, usar essa ferramenta foi um pouco chato.

Aqui devo admitir que a minha incerteza reside muito provavelmente na minha falta de familiaridade com ansibledo que em suas deficiências. E isso, aliás, é um ponto importante. ansible é uma área de conhecimento completamente separada e própria com seu próprio DSL (Domain Specific Language), que deve ser mantido em um nível de confiança. Bem, naquele momento que ansible Ele está se desenvolvendo rapidamente e, sem consideração especial pela compatibilidade com versões anteriores, não acrescenta confiança.

Portanto, não faz muito tempo, uma segunda versão da bicicleta foi implementada. Desta vez python, ou melhor, em uma estrutura escrita em python e para python intitulado Nornir

Então - Nornir é um microframework escrito em python e para python e projetado para automação. O mesmo que no caso com ansible, para resolver os problemas aqui, é necessária uma preparação de dados competente, ou seja, inventário de hosts e seus parâmetros, mas os scripts não são escritos em uma DSL separada, mas no mesmo p[i|i]ton não muito antigo, mas muito bom.

Vejamos o que é usando o seguinte exemplo ao vivo.

Tenho uma rede de agências com várias dezenas de escritórios em todo o país. Cada escritório possui um roteador WAN que termina vários canais de comunicação de diferentes operadoras. O protocolo de roteamento é BGP. Os roteadores WAN vêm em dois tipos: Cisco ISG ou Juniper SRX.

Agora a tarefa: você precisa configurar uma sub-rede dedicada para Videovigilância em uma porta separada em todos os roteadores WAN da rede da filial - anunciar esta sub-rede no BGP - configurar o limite de velocidade da porta dedicada.

Primeiro, precisamos preparar alguns modelos, com base nos quais as configurações serão geradas separadamente para Cisco e Juniper. Também é necessário preparar dados para cada ponto e parâmetros de conexão, ou seja, coletar o mesmo inventário

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

Modelo para Junípero:

$ 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

Os modelos, é claro, não surgem do nada. Estas são essencialmente diferenças entre as configurações de trabalho que existiam e existiam após a resolução da tarefa em dois roteadores específicos de modelos diferentes.

Em nossos templates vemos que para resolver o problema, precisamos apenas de dois parâmetros para Juniper e 3 parâmetros para Cisco. aqui estão eles:

  • ifnome
  • sufixo ip
  • asn

Agora precisamos definir esses parâmetros para cada dispositivo, ou seja, Faça a mesma coisa inventário.

Para inventário Seguiremos rigorosamente a documentação Inicializando Nornir

isto é, vamos criar o mesmo esqueleto de arquivo:

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

O arquivo config.yaml é o arquivo de configuração padrão do 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 os principais parâmetros do arquivo hosts.yaml, grupo (no meu caso são logins/senhas) em grupos.yamle em padrões.yaml Não indicaremos nada, mas você precisa inserir três pontos negativos - indicando que é yaml o arquivo está vazio.

Esta é a aparência de 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

E aqui está groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Isso é o que aconteceu inventário para a nossa tarefa. Durante a inicialização, os parâmetros dos arquivos de inventário são mapeados para o modelo de objeto Elemento de inventário.

Abaixo do spoiler está um diagrama do 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 pode parecer um pouco confuso, principalmente no início. Para descobrir isso, o modo interativo em Pitão.

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

E finalmente, vamos passar para o script em si. Não tenho nada do que me orgulhar particularmente aqui. Acabei de pegar um exemplo pronto de tutorial e usei-o quase inalterado. Esta é a aparência do script de trabalho finalizado:

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 atenção no parâmetro corrida_seca = Verdadeiro inicialização do objeto em linha nr.
Aqui o mesmo que em ansible foi implementado um teste no qual é feita uma conexão ao roteador, uma nova configuração modificada é preparada, que é então validada pelo dispositivo (mas isso não é certo; depende do suporte do dispositivo e da implementação do driver no NAPALM) , mas a nova configuração não é aplicada diretamente. Para uso em combate, você deve remover o parâmetro funcionamento a seco ou altere seu valor para Falso.

Quando o script é executado, o Nornir envia logs detalhados para o console.

Abaixo do spoiler está o resultado de uma execução de combate em dois roteadores de teste:

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

Escondendo senhas em ansible_vault

No início do artigo exagerei um pouco ansible, mas não é tão ruim assim. eu realmente gosto deles Abóbada like, que é projetado para ocultar informações confidenciais. E provavelmente muitos notaram que temos todos os logins/senhas de todos os roteadores de combate brilhando em formato aberto em um arquivo gorups.yaml. Não é bonito, claro. Vamos proteger esses dados com Abóbada.

Vamos transferir os parâmetros de groups.yaml para creds.yaml e criptografá-los com AES256 com uma senha 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

É simples assim. Resta ensinar nossos Nornir-script para recuperar e aplicar esses dados.
Para fazer isso, em nosso script após a linha de inicialização nr = InitNornir(config_file=… adicione o seguinte 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
...

Obviamente, vault.passwd não deve estar localizado próximo a creds.yaml como no meu exemplo. Mas está tudo bem para jogar.

É tudo por agora. Há mais alguns artigos sobre Cisco + Zabbix chegando, mas não se trata nem um pouco sobre automação. E num futuro próximo pretendo escrever sobre RESTCONF na Cisco.

Fonte: habr.com

Adicionar um comentário