Gerando configurações usando Jinja2 e YML

Bernardo Soares
TechRebels
11 min readMar 21, 2019

--

Atualmente, as infraestruturas de rede já não são mais estáticas, o conceito de “colocar a feature ali e não mexe nunca mais” está caindo em desuso. Para atender as mais diversas demandas, a infraestrutura precisa se adaptar de uma forma dinâmica, mas ainda mantendo uma certa estabilidade. Alcançar este equilíbrio é, no mínimo, extremamente desafiador.

Isso também significa que, quanto maior a necessidade de interagir com o estado da infraestrutura, maior a probabilidade de algum componente ser afetado de uma forma “inesperada”. Segundo esse artigo no network world, um dos maiores motivos de downtime na rede é o fator humano.

Bem, muito mais frequentemente do que esperamos, iremos deixar passar um ou dois erros de sintaxe em uma configuração. Em alguns casos, um pequeno erro gera um impacto enorme (switchport trunk allowed a̵d̵d̵).

O mesmo se aplica à verificação e monitoramento. Quando reduzimos o contato humano, ou adicionamos uma camada de software entre o input de um operador e a configuração final de um dispositivo, este risco pode ser reduzido — e muito! Assim podemos realizar alterações com muito mais flexibilidade e confiança, sem aquele receio de apertar o commit.

Hoje vamos ver como podemos gerar, de forma “dinâmica”, configurações de equipamentos de rede. Quando usamos o Python, estruturamos os dados usando sintaxe yaml, e renderizamos estes dados em um template jinja2, podemos gerar configurações similares independente da sintaxe CLI de um determinado vendor.

Falar sobre o básico em como o Python funciona e seus benefícios estão fora do escopo. Vamos direto ao que interessa :)

Entendendo uma configuração

Quando analisamos uma configuração de um equipamento, podemos perceber que existem duas partes:

1- Sintaxe do NOS (network OS)

2- Elementos específicos da configuração, o que sempre será variável (ex: bgp asn, ospf PID, string da description, etc).

router ospf 69
router-id
1.2.3.4
redistribute bgp
65534 metric-type 1 subnets route-map REDIST
passive-interface
Loopback0
router bgp 65534
bgp log-neighbor-changes
neighbor
3.2.1.1 remote-as 64512
neighbor
3.2.1.1 description MY-PEER

Na configuração acima, podemos ver que o que está marcado em itálico pertence a estrutura sintática implementada pelo sistema operacional. Já o que está marcado em negrito, é algo que é apenas recebido e interpretado pelo parser — o equipamento apenas toma aquilo como um argumento e implementa a funcionalidade como desejado.

Dito isto, automatizar o processo de gerar uma determinada config pode ser entendido como “preencher as lacunas”. A sintaxe será sempre a mesma, porém devemos escolher quais argumentos passar para o equipamento.

Em umas outras palavras, a parte da sintaxe será implementada utilizando o jinja2, e os argumentos serão definidos em um arquivo yml, utilizando formato yaml.

YAML

YAML, ou (YAML Ain’t Markup Language) é um formato de serialização de dados. Este modelo é muito utilizado pela simplicidade de sua sintaxe, sendo bem mais amigável do que os bem conhecidos XML (ARGH!!!!) e JSON.

Utilizando o yaml, podemos definir uma estrutura básica de dados para que possamos montar uma determinada config para um determinado grupo de variáveis (ou seja, um determinado equipamento).

Vamos supor que temos um determinado router, com as seguintes características:

hostname - router 1
bgp AS - 64512
router-reflector? sim
neighbors: 10.0.0.1, 10.0.0.2, 10.0.0.3

Podemos, de uma forma simples, traduzir estes elementos para a sintaxe yaml:

---
DEVICE:
hostname: router 1
ROUTING:
bgp asn: 64512
router reflector: True
neighbors:
- 10.0.0.1
- 10.0.0.2
- 10.0.0.3

Note que podemos utilizar este formato para definir qualquer coisa. Neste momento, o modelo de dados em yaml não tem nada a ver com a config que vamos gerar — se trata apenas de como estamos estruturando nossos dados. Isso significa que, com um pouco de esforço, podemos ilustrar uma configuração completa utilizando esta linguagem (acl, route-policies, interfaces, etc etc etc).

De outro ponto de vista, o bloco yaml acima pode ser interpretado como uma série de dicionários e listas organizados, o que facilita a interação com esta estrutura de dados usando o Python e Jinja.

O resultado que teremos seria equivalente ao seguinte dicionário em Python:

{'DEVICE': {'hostname': 'router 1'},
'ROUTING': {'bgp asn': 64512,
'neighbors': ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
'router reflector': True}}

Jinja2

O Jinja é uma linguagem usada para criar templates, e seu uso é amplamente difundido no mundo de UI (user interface) e Web Dev, em conjunto com o HTML.

Utilizando Jinja, podemos inserir variáveis em um arquivo base (o que chamamos de template), o que nos permite utilizar apenas um template para gerar diversos resultados, o que seria a nossa configuração.

Exemplo da sintaxe:

texto no template {{ variável }}
{# comentário. abaixo teremos uma estrutura de controle #}
{% for variable in mydict %}
{% if variable.name is defined %}
use-this-variable {{ variable.name|upper() }}
{% endif %}
{% endfor %}

Sem mais exemplos, podemos ver que temos uma enorme flexibilidade combinando as strings no template (o que geralmente será parte da sintaxe do NOS) com as variáveis (obtidas através de outro meio, como um arquivo .yml). Para facilitar ainda mais, o Jinja ainda nos permite utilizar de algumas funções built-in do Python! (upper(), lower(), etc)

Na prática

Então vamos lá. No exemplo de hoje, vamos utilizar o Python e as bibliotecas yamlreader e jinja2. De forma automatizada, vamos gerar partes importantes da configuração de um dispositivo.

Nota: As configurações apresentadas são meramente ilustrativas, e podem ou não conter erros de sintaxe e/ou lógica. Em outras palavras, não testei em um equipamento real :)

Suponha que tenhamos diversos sites, onde a topologia de rede é idêntica. Algo bem simples, como um modelo de três camadas com o core ‘colapsado’. Nesta topologia, temos um elemento que chamaremos de “CORE”, implementando as funções de core e agregação do site. Neste elemento, teremos três vlans:

vlan10 — Voz
vlan100 — Dados
vlan 999 — Mgmt (Gerência)

Nossos equipamentos implementarão as funções de gateway para todas estas vlans. Adicionalmente, teremos também uma sessão BGP com o router da nossa operadora fictícia para conectarmos à internet e VPN (MPLS). Para complicar um pouco, trabalharemos com um equipamento Cisco (um 3850, por exemplo) e um Juniper (sei lá, um MX).

Sites e seus endereçamentos:

RDJ:
vlan10–192.168.0.0/24
vlan100–192.168.1.0/24
vlan999–172.16.21.0/24
P2P BGP: 100.64.21.1/31
BHZ:
vlan10–192.168.4.0/24
vlan100–192.168.5.0/24
vlan999–172.16.31.0/24
P2P BGP: 100.64.31.1/31

A definição das características em linguagem yml será similar para ambos, pois implementarão exatamente as mesmas funções.

cat core-rdj.yml---
hostname: core-rdj-cisco
NETWORKS:
- vlan_id: 10
description: Voice
addr: 192.168.0.1
mask: 255.255.255.0
- vlan_id: 100
description: Data
addr: 192.168.1.1
mask: 255.255.255.0
- vlan_id: 999
description: Management
addr: 172.16.21.1
mask: 255.255.255.0
- port_id: 47
description: P2P-BGP
addr: 100.64.21.1
mask: 255.255.255.254
ROUTING:
bgp_asn: 64512
redistribute_connected: True
neighbor_groups:
- name: MY_PROVIDER
asn: 65000
allow_as: 2
policy: INTERNAL-TO-VPN
neighbors:
- 100.64.21.0
SWITCHPORTS:
- port_id: 46
description: sw1-access-rdj
allowed_vlans:
- 10
- 100
- 999
- port_id: 45
description: sw2-access-rdj
allowed_vlans:
- 10
- 100
- 999

Agora BHZ, que será um Juniper:

cat core-bhz.yml---hostname: core-bhz-junosNETWORKS:
- vlan_id: 10
description: Voice
addr: 192.168.4.1
prefix_len: 24
- vlan_id: 100
description: Data
addr: 192.168.5.1
prefix_len: 24
- vlan_id: 999
description: Management
addr: 172.16.31.1
prefix_len: 24
- port_id: 47
description: P2P-BGP
addr: 100.64.31.1
prefix_len: 31
ROUTING:
bgp_asn: 64512
redistribute_connected: True
neighbor_groups:
- name: MY_PROVIDER
asn: 65000
allow_as: 2
policy: INTERNAL-TO-VPN
neighbors:
- 100.64.31.0
SWITCHPORTS:
- port_id: 46
description: sw1-access-bhz
allowed_vlans:
- 10
- 100
- 999
- port_id: 45
description: sw2-access-bhz
allowed_vlans:
- 10
- 100
- 999

Estes arquivos yaml, quando interpretados utilizando o método yaml_load, se torna um dicionário, organizando seus pares “key, value” em uma forma hierárquica que corresponde ao estilo que foi definido utilizando a linguagem.

Agora, vamos criar um template de configuração. Para acessar os elementos do dicionário acima, vamos montar o template utilizando as tags do Jinja2. No Jinja, conseguimos utilizar controles de fluxo e condicionais (for, if) e booleans (if X == True); o que nos dá flexibilidade para repetirmos uma mesma sequência (diversas interfaces, por exemplo) ou tratar exceções (caso valor seja X, associe Y).

cat cisco.j2

!
hostname {{ hostname }}
!
{%- for network in NETWORKS %}
{%- if network.vlan_id is defined %}
interface Vlan{{ network.vlan_id }}
{%- elif network.port_id is defined %}
interface GigabitEthernet0/{{ network.port_id }}
{%- endif %}
description {{ network.description }}
ip address {{ network.addr }} {{ network.mask }}
!
{%- endfor %}
!
route-policy INTERNAL-TO-VPN permit 1
exit
!
router bgp {{ ROUTING.bgp_asn }}
{%- if ROUTING.redistribute_connected == True %}
redistribute connected
{%- endif %}
{%- for GROUP in ROUTING.neighbor_groups %}
neighbor {{ GROUP.name }} peer-group
neighbor {{ GROUP.name }} route-map {{ GROUP.policy }} out
neighbor {{ GROUP.name }} remote-as {{ GROUP.asn }}
{%- if GROUP.allow_as is defined %}
neighbor {{ GROUP.name }} allow-as in {{ GROUP.allow_as }}
{%- endif %}
!
{%- for NEIGHBOR in GROUP.neighbors %}
neighbor {{ NEIGHBOR }} group {{ GROUP.name }}
!
{%- endfor %}
{%- endfor %}
!
{%- for PORT in SWITCHPORTS %}
interface GigabitEthernet0/{{ PORT.port_id }}
description {{ PORT.description }}
switchport
switchport trunk allowed vlan {{ PORT.allowed_vlans|join(',') }}
!
{%- endfor%}
!
!
junos.j2{%- for network in NETWORKS %}
{%- if network.vlan_id is defined %}
set bridge-domains VLAN{{network.vlan_id}} description {{ network.description }}
set bridge-domains VLAN{{network.vlan_id}} domain-type bridge
set bridge-domains VLAN{{network.vlan_id}} vlan-id {{network.vlan_id}}
set bridge-domains VLAN{{network.vlan_id}} routing-interface irb.{{network.vlan_id}}
!
set policy-options policy-statement {{ network.description }} from route-filter {{ network.addr }}/{{ network.prefix_len }}
set policy-options policy-statement {{ network.description }} then accept
!
set interfaces irb unit {{network.vlan_id}} description {{ network.description }}
set interfaces irb unit {{network.vlan_id}} family inet address {{ network.addr }}/{{ network.prefix_len }}
!
{%- elif network.port_id is defined %}
set interfaces ge-0/0/{{ network.port_id }} unit 0 family inet address {{ network.addr }}/{{ network.prefix_len }}
set interfaces ge-0/0/{{ network.port_id }} description {{ network.description }}
!
{%- endif %}
{%- endfor %}
!
set policy-options policy-statement INTERNAL-TO-VPN term ALL then accept
!
set protocols bgp family inet unicast
{%- for GROUP in ROUTING.neighbor_groups %}
set protocols bgp group {{ GROUP.name }} type external
set protocols bgp group {{ GROUP.name }} peer-as {{ GROUP.asn }}
set protocols bgp group {{ GROUP.name }} family inet unicast loops {{ GROUP.allow_as }}
{%- for NEIGHBOR in GROUP.neighbors %}
set protocols bgp group {{ GROUP.name }} neighbor {{ NEIGHBOR }} local-as {{ ROUTING.bgp_asn }}
{%- endfor %}
{%- endfor %}
!
!
{%- for PORT in SWITCHPORTS %}
set interfaces ge-0/0/{{ PORT.port_id }} unit 0 description {{ PORT.description }}
set interfaces ge-0/0/{{ PORT.port_id }} unit 0 encapsulation vlan-bridge
set interfaces ge-0/0/{{ PORT.port_id }} unit 0 family bridge interface-mode trunk
{%- for VLAN in PORT.allowed_vlans %}
set interfaces ge-0/0/{{ PORT.port_id }} unit 0 family bridge vlan-id-list {{ VLAN }}
{%- endfor %}
{%- endfor %}

Por fim, vamos utilizar um breve script em python, que irá executar basicamente as seguintes funções:

  • ler arquivo yml, interpretar em uma estrutura de tipo “dict”(módulo yamlreader)
  • renderizar o template, utilizando os valores acima (módulo jinja2)
  • gravar o resultado, ou printar na tela
cat builder.py#! /usr/bin/env/pythonfrom jinja2 import Environment, FileSystemLoader
from yamlreader import yaml_load
# here we define our devices and types
devices = {"core-bhz": "junos", "core-rdj": "cisco"}
for device, type in devices.iteritems():
try:
# Loading yaml from current dir
file = device + ".yml"
devyaml = yaml_load(file)
# Now invoking the template
file_loader = FileSystemLoader('.')
env = Environment(loader=file_loader)
template = env.get_template(type + ".j2")
# Render!
output = template.render(devyaml)
# Save to file
with open(device + ".cfg", "w") as outfile:
outfile.write(output)
# Print result
print output
print "Configuration built for {}. Filename: ./{}".format(device, device + ".cfg")
except Exception as e: print "Error while building config! Error details:\n {}".format(e)

Script bem simples. Definimos os dois equipamentos e seu respectivo OS/vendor, procuramos pelos respectivos yml e j2, renderizamos o template e escrevemos em um arquivo chamado <device>.cfg.

Quando executamos o script, eis o resultado que temos para ambos os devices:

» python builder.py                                           
!
hostname core-rdj-cisco
!
interface Vlan10
description Voice
ip address 192.168.0.1 255.255.255.0
!
interface Vlan100
description Data
ip address 192.168.1.1 255.255.255.0
!
interface Vlan999
description Management
ip address 172.16.21.1 255.255.255.0
!
interface GigabitEthernet0/47
description P2P-BGP
ip address 100.64.21.1 255.255.255.254
!
!
route-policy INTERNAL-TO-VPN permit 1
exit
!
router bgp 64512
redistribute connected
neighbor MY_PROVIDER peer-group
neighbor MY_PROVIDER route-map INTERNAL-TO-VPN out
neighbor MY_PROVIDER remote-as 65000
neighbor MY_PROVIDER allow-as in 2
!
neighbor 100.64.21.0 group MY_PROVIDER
!
!
interface GigabitEthernet0/46
description sw1-access-rdj
switchport
switchport trunk allowed vlan 10,100,999
!
interface GigabitEthernet0/45
description sw2-access-rdj
switchport
switchport trunk allowed vlan 10,100,999
!
!
!
Configuration built for core-rdj. Filename: ./core-rdj.cfg
set system hostname core-bhz-junos
!
set bridge-domains VLAN10 description Voice
set bridge-domains VLAN10 domain-type bridge
set bridge-domains VLAN10 vlan-id 10
set bridge-domains VLAN10 routing-interface irb.10
!
set interfaces irb unit 10 description Voice
set interfaces irb unit 10 family inet address 192.168.4.1/24
!
set bridge-domains VLAN100 description Data
set bridge-domains VLAN100 domain-type bridge
set bridge-domains VLAN100 vlan-id 100
set bridge-domains VLAN100 routing-interface irb.100
!
set interfaces irb unit 100 description Data
set interfaces irb unit 100 family inet address 192.168.5.1/24
!
set bridge-domains VLAN999 description Management
set bridge-domains VLAN999 domain-type bridge
set bridge-domains VLAN999 vlan-id 999
set bridge-domains VLAN999 routing-interface irb.999
!

set interfaces irb unit 999 description Management
set interfaces irb unit 999 family inet address 172.16.31.1/24
!
set interfaces ge-0/0/47 unit 0 family inet address 100.64.31.1/31
set interfaces ge-0/0/47 description P2P-BGP
!
set policy-options policy-statement INTERNAL-TO-VPN term ALL then accept
!
set protocols bgp family inet unicast
set protocols bgp group MY_PROVIDER type external
set protocols bgp group MY_PROVIDER export policy INTERNAL-TO-VPN
set protocols bgp group MY_PROVIDER peer-as 65000
set protocols bgp group MY_PROVIDER family inet unicast loops 2
set protocols bgp group MY_PROVIDER neighbor 100.64.31.0 local-as 64512
!
!
set interfaces ge-0/0/46 unit 0 description sw1-access-bhz
set interfaces ge-0/0/46 unit 0 encapsulation vlan-bridge
set interfaces ge-0/0/46 unit 0 family bridge interface-mode trunk
set interfaces ge-0/0/46 unit 0 family bridge vlan-id-list 10
set interfaces ge-0/0/46 unit 0 family bridge vlan-id-list 100
set interfaces ge-0/0/46 unit 0 family bridge vlan-id-list 999
set interfaces ge-0/0/45 unit 0 description sw2-access-bhz
set interfaces ge-0/0/45 unit 0 encapsulation vlan-bridge
set interfaces ge-0/0/45 unit 0 family bridge interface-mode trunk
set interfaces ge-0/0/45 unit 0 family bridge vlan-id-list 10
set interfaces ge-0/0/45 unit 0 family bridge vlan-id-list 100
set interfaces ge-0/0/45 unit 0 family bridge vlan-id-list 999
!
!
Configuration built for core-bhz. Filename: ./core-bhz.cfg

Mais fácil do que parece, não é?

Utilizando esta mesma lógica, alinhado a um sistema de versionamento (como o Git, por exemplo), conseguimos montar um sistema de controle e automatização de configurações bem estável. Os dias de fazer backup de configuração via “ telnet, show run “, estão contados!

Conclusão

No post de hoje falamos um pouco sobre como podemos automatizar facilmente a criação de configurações de elementos de rede de uma forma rápida e fácil utilizando YAML, Jinja2 e Python (usando as bibliotecas yamlreader e jinja2), bem como sobre algumas vantagens de se automatizar esta parte do processo, reduzindo o tempo necessário para gerar a configuração e garantindo um resultado consistente — o que reduz significativamente a probabilidade de erros e typos no processo.

Se gostou do conteúdo, peço para compartilhar com outros do ramo. Não se esqueça de seguir a mim e ao TechRebels clicando follow aí embaixo :)

Sobre o autor:

Bernardo, CCIE #57862

Cloud Network Engineer

linkedin.com/in/bernardosoares/

--

--