
Olá Habr!
Recentemente, um artigo de Mikrotik e Linux apareceu aqui . Rotina e automação onde um problema semelhante foi resolvido por meios fósseis. E embora a tarefa seja completamente típica, Habré não encontra nada parecido. Atrevo-me a oferecer minha bicicleta à respeitável comunidade de TI.
Esta não é a primeira bicicleta para esta tarefa. A primeira opção foi implementada há vários anos na versão ansible 1.x.x. A bicicleta era raramente usada e, portanto, constantemente enferrujada. No sentido de que a tarefa em si não ocorre com a mesma frequência que as versões atualizadas do ansible . E toda vez que você precisar ir, a corrente cairá, a roda cairá. No entanto, a primeira parte, a geração de configurações - sempre funciona com muita clareza, pois o mecanismo foi estabelecido há muito tempo. Mas a segunda parte - lançando configurações, como regra, trouxe surpresas. E como tenho que implantar a configuração remotamente no chão de centenas de dispositivos, alguns dos quais estão a milhares de quilômetros de distância, o uso dessa ferramenta foi um pouco assustador.
Aqui devo admitir que minha insegurança reside antes na falta de familiaridade comigo, do que em suas deficiências. E isso, a propósito, é um ponto importante. ansible é um domínio de conhecimento próprio completamente separado, com sua própria DSL (Domain Specific Language), que deve ser mantida em um nível confiável. Bem, o momento em que o ansible está se desenvolvendo rapidamente, e sem atenção especial à compatibilidade com versões anteriores, não adiciona confiança.
Portanto, não faz muito tempo, a segunda versão da bicicleta foi implementada. Desta vez em python , ou melhor, em uma estrutura escrita em python e para python chamada Nornir
So - Nornir é uma microfrabalhagem escrita em python e para python e destinada à automação. Como no caso do ansible , para resolver problemas, aqui é necessária a preparação competente de dados, isto é, inventário de hosts e seus parâmetros, mas os scripts não são gravados em uma DSL separada, mas todos no mesmo tom não muito antigo, mas muito bom [e | ah].
Vejamos o que é o seguinte exemplo vivo.
Eu tenho uma rede de agências com várias dezenas de escritórios em todo o país. Em cada escritório, há um roteador WAN que encerra vários canais de comunicação de diferentes operadoras. O protocolo de roteamento é BGP. Existem dois tipos de roteadores WAN: Cisco ISG ou Juniper SRX.
Agora a tarefa: é necessário configurar uma sub-rede dedicada para vigilância por vídeo em uma porta separada em todos os roteadores WAN da rede da filial - anuncie esta sub-rede no BGP - configure o limite de velocidade para a porta dedicada.
Primeiro, precisamos preparar alguns modelos, com base nas configurações que serão geradas separadamente para Cisco e Juniper. E também é necessário preparar dados para cada ponto e parâmetros de conexão, ou seja, para coletar esse 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 o 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
Os modelos, é claro, não são retirados do teto. Essa é essencialmente a diferença entre as configurações de trabalho - foi depois de resolver a tarefa em dois roteadores específicos de modelos diferentes.
Nos nossos modelos, vemos que, para resolvermos o problema, dois parâmetros para o Juniper e três para a Cisco são suficientes. aqui estão eles:
Agora, precisamos definir esses parâmetros para cada dispositivo, ou seja, faça o mesmo inventário .
Para o inventário , seguiremos claramente a seção Inicializando o Nornir
ou seja, crie o mesmo esqueleto de arquivo:
. ├── config.yaml ├── inventory │ ├── defaults.yaml │ ├── groups.yaml │ └── hosts.yaml
Arquivo Config.yaml - 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"
Vamos especificar os principais parâmetros no arquivo hosts.yaml , group (no meu caso, nomes de usuário / senhas) em groups.yaml , e não especificaremos nada em defaults.yaml , mas deve haver três menos para indicar que este é um arquivo yaml embora vazio.
É assim que o hosts.yaml se parece:
--- 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
Aqui está o inventário da nossa tarefa. Durante a inicialização, os parâmetros dos arquivos de inventário são mapeados para o modelo de objeto InventoryElement .
Sob o spoiler, o 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, especialmente no início. O modo interativo no ipython ajuda muito para descobrir isso.
$ 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'
Bem, finalmente, vamos seguir para o próprio script. Não há nada de especial para eu me orgulhar. Eu apenas peguei o exemplo final do tutorial e o usei quase sem alterações. É assim que o script de trabalho final se parece:
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):
Observe o parâmetro dry_run = True na linha de inicialização do objeto nr .
Aqui, assim como é possível , é executada uma execução de teste na qual a conexão ao roteador é feita, uma nova configuração alterada é preparada, que é validada pelo dispositivo (mas isso não é exato; depende do suporte 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 dry_run ou alterar seu valor para False .
Quando o script é executado, o Nornir exibe logs detalhados no console.
Sob o spoiler, a conclusão do combate é executada 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ocultando senhas no ansible_vault
No começo do artigo, fiquei um pouco ansioso , mas nem tudo é tão ruim lá. Eu realmente gosto do cofre , projetado para esconder informações confidenciais fora da vista. E provavelmente muitos notaram que temos todos os nomes de usuário / senhas para todos os roteadores de batalha brilhando no formulário aberto no arquivo groups.yaml . É feio, é claro. Vamos proteger esses dados com o vault .
Transferimos os parâmetros de groups.yaml para creds.yaml e criptografamos 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
Tão simples. Resta ensinar nosso script Nornir a obter e usar 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)
Obviamente, o vault.passwd não deve estar ao lado de creds.yaml, como no meu exemplo. Mas, para jogar, serve.
Por enquanto é tudo. Alguns artigos sobre o Cisco + Zabbix estão chegando, mas isso não é um pouco sobre automação. E, em um futuro próximo, pretendo escrever sobre o RESTCONF na Cisco.