
Olá, Habr!
Recentemente, apareceu um artigo aqui onde tarefas semelhantes foram resolvidas com ferramentas arcaicas. E embora a tarefa seja totalmente típica, não encontrei nada parecido sobre isso no Habr. Permito-me sugerir à respeitável comunidade de TI minha própria bicicleta.
Esta não é a primeira bicicleta para tal tarefa. A primeira versão foi implementada há alguns anos ainda na ansible versão 1.x.x. A bicicleta foi pouco utilizada e, portanto, ficou constantemente enferrujando. No sentido de que a própria tarefa não ocorre tão frequentemente quanto as versões são atualizadas. ansible. E cada vez que preciso usá-la, ou a corrente cai, ou a roda se solta. No entanto, a primeira parte, a geração de configurações, sempre funciona muito bem, felizmente, jinja2 o motor já está bem estabelecido. Mas a segunda parte — a aplicação das configurações, geralmente apresenta surpresas. E como preciso aplicar a configuração remotamente em cerca de cinquenta dispositivos, alguns dos quais estão a milhares de quilômetros de distância, usar essa ferramenta era um pouco assustador.
Aqui é preciso reconhecer que minha insegurança provavelmente decorre da minha falta de familiaridade com ansible, do que de suas deficiências. E isso, aliás, é um ponto importante. ansible — é uma área de conhecimento totalmente distinta, com sua própria DSL (Linguagem Específica de Domínio), que precisa ser mantida em um nível de confiabilidade. Além disso, o fato de que ansible se desenvolve rapidamente, sem muita preocupação com a compatibilidade retroativa, não ajuda a aumentar a confiança.
Por isso, não faz muito tempo, foi implementada uma segunda versão da bicicleta. Desta vez sobre python, ou melhor, sobre um framework escrito em python e para python chamada
Então — Nornir é um microframework escrito em python e para python e destinado à automação. Assim como no caso de ansible, para resolver tarefas aqui é necessária uma preparação de dados adequada, ou seja, o inventário de hosts e seus parâmetros, mas os scripts são escritos não em uma DSL separada, mas todos na mesma, não muito antiga, mas bastante amigável, linguagem Python.
Vamos ver o que é isso em um exemplo prático.
Tenho uma rede filial com várias dezenas de escritórios em todo o país. Em cada escritório, há um roteador WAN que termina vários canais de comunicação de diferentes operadores. O protocolo de roteamento é BGP. Os roteadores WAN vêm em dois tipos: Cisco ISG ou Juniper SRX.
Agora a tarefa é: configurar em todos os roteadores WAN da rede filial uma sub-rede dedicada para Videovigilância em uma porta separada — anunciar essa sub-rede no BGP — configurar a limitação de velocidade da porta dedicada.
Primeiro, precisamos preparar alguns modelos, com os quais as configurações serão geradas separadamente para Cisco e Juniper. Também precisamos coletar os dados de cada ponto e os parâmetros de conexão, ou seja, reunir o chamado 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 anyModelo para Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Videovigilância"
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 limiterOs modelos, naturalmente, não vêm do nada. Eles são essencialmente diffs entre as configurações de trabalho que foram realizadas e o estado atual após a execução da tarefa em dois roteadores específicos de diferentes modelos.
A partir de nossos modelos, vemos que para resolver a tarefa precisamos de dois parâmetros para Juniper e três parâmetros para Cisco. Aqui estão eles:
- ifname
- ipsuffix
- asn
Agora precisamos definir esses parâmetros para cada dispositivo, ou seja, fazer o mesmo. inventory.
Para inventory vamos seguir rigorosamente a documentação.
ou seja, criaremos uma estrutura de arquivos semelhante:
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlO 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"Vamos indicar os parâmetros principais no arquivo hosts.yaml, grupos (no meu caso isso são logins/senhas) em groups.yaml, e no defaults.yaml não precisamos especificar nada, mas é necessário colocar três sinais de menos — indicando que isso é yaml o arquivo, embora esteja vazio.
Assim é como o hosts.yaml se apresenta:
---
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: 65111E assim é o groups.yaml:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2Assim ficou inventory para nossa tarefa. Ao inicializar, os parâmetros dos arquivos de inventário são mapeados para o modelo de objeto InventoryElement.
Sob o spoiler, o esquema 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. Para entender, o modo interativo ajuda muito em ipython.
$ ipython3
Python 3.6.9 (padrão, 7 Nov 2019, 10:44:02)
Digite 'copyright', 'credits' ou 'license' para mais informações
IPython 7.1.1 -- Um Python Interativo aprimorado. Digite '?' para ajuda.
Em [1]: from nornir import InitNornir
Em [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)
Em [3]: nr.inventory.hosts
Saída[3]:
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}
Em [4]: nr.inventory.hosts['srx-test'].data
Saída[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}
Em [5]: nr.inventory.hosts['srx-test']['task_data']
Saída[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}
Em [6]: nr.inventory.hosts['srx-test'].platform
Saída[6]: 'junos'
E finalmente, chegamos ao script em si. Aqui, não tenho muito a me orgulhar. Eu apenas peguei um exemplo pronto do e o utilizei praticamente sem mudanças. Veja como é o script de trabalho pronto:
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 os dados do inventário em configuração através de um arquivo de template
r = task.run(task=text.template_file,
name="Configuração Base",
template="base.j2",
path=f"templates/{task.host.platform}")
# Salvar a configuração compilada em uma variável do host
task.host["config"] = r.result
# Salvar a configuração compilada em um arquivo
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Implementar essa configuração no dispositivo usando NAPALM
task.run(task=networking.napalm_configure,
name="Carregando a Configuração no dispositivo",
replace=False,
configuration=task.host["config"])
nr = InitNornir(config_file="config.yaml", dry_run=True) # defina dry_run=False, cruze os dedos e execute novamente
# executar tarefas
result = nr.run(task=config_and_deploy)
print_result(result)Preste atenção no parâmetro dry_run=True na linha de inicialização do objeto nr.
Aqui também está como no ansible é realizado um teste na qual ocorre a conexão com o roteador, uma nova configuração modificada é preparada, que é então validada pelo dispositivo (mas isso não é preciso; depende do suporte do dispositivo e da implementação do driver no NAPALM), mas a aplicação da nova configuração não acontece de fato. Para uma aplicação real, é necessário remover o parâmetro dry_run ou mudar seu valor para False.
Ao executar o script, o Nornir exibe logs detalhados no console.
Sob o spoiler, a saída da execução em dois roteadores de teste:
config_and_deploy***************************************************************
* cisco-test ** alterado: Verdadeiro *******************************************
vvvv config_and_deploy ** alterado: Verdadeiro vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuração Base ** alterada: Verdadeiro ------------------------------------- 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
---- Carregando Configuração no dispositivo ** alterado: Verdadeiro --------------------- 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
^^^^ FIM config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** alterado: Verdadeiro *******************************************
vvvv config_and_deploy ** alterado: Verdadeiro vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuração Base ** alterada: Verdadeiro ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Videovigilância"
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
---- Carregando Configuração no dispositivo ** alterado: Verdadeiro --------------------- INFO
[edit interfaces]
+ fe-0/0/2 {
+ unit 0 {
+ description "Videovigilância";
+ 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;
+ }
+ }
+ }
+ }
^^^^ FIM config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Escondendo senhas no ansible_vault
No início do artigo, eu fiz algumas críticas a ansible, mas a situação não é tão ruim. Eu realmente gosto deles vault é uma forma de ocultar informações sensíveis dos olhos. E provavelmente muitos notaram que todos os nossos logins/senhas para todos os roteadores de produção aparecem em aberto em um arquivo. gorups.yaml. É feio, sem dúvida. Vamos proteger esses dados usando vault.
Vamos transferir os parâmetros de groups.yaml para creds.yaml e criptografá-lo 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
Criptografia bem-sucedida
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435Pronto. Agora só precisamos ensinar nosso Nornir-script a acessar e aplicar esses dados.
Para isso, em nosso script, após a linha de inicialização nr = InitNornir(config_file=… adicionamos o seguinte código:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # defina dry_run=False, cruze os dedos e execute novamente
# enriqueça o inventário com os dados criptografados do 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))
# executar tarefas
...É claro que vault.passwd não deve estar próximo a creds.yaml como no meu exemplo. Mas para brincar, está bom.
Por enquanto é isso. Em breve, publicarei mais alguns artigos sobre Cisco + Zabbix, mas isso é um pouco diferente de automação. E em um futuro próximo, planejo escrever sobre RESTCONF no Cisco.
Fonte: habr.com
