
Salut, Habr!
Recent articles have appeared here where similar tasks were solved using outdated methods. And although the task is quite typical, I couldn't find anything similar about it on Habrahabr. I dare to propose my own solution to the esteemed IT community.
This is not the first solution for such a task. The first version was implemented several years ago on ansible version 1.x.x. The solution was rarely used and thus it became rusty. In the sense that the task itself does not arise as frequently as the versions are updated ansible. And each time when it needs to be run, either the chain falls off or the wheel detaches. However, the first part, configuration generation, always works very smoothly, thanks to jinja2 which has long established itself. But the second part—deploying configurations—often presented surprises. Since I have to deploy configurations remotely on several dozen devices, some of which are thousands of kilometers away, using this tool was a bit daunting.
Here I must admit that my insecurity stems more from my insufficient acquaintance with ansible, than from its shortcomings. And by the way, this is an important point. ansible — it is a completely separate, its own area of knowledge with its own DSL (Domain Specific Language) that must be maintained at a confident level. And the fact that ansible develops quite rapidly, and without much regard for backward compatibility, does not add to my confidence.
That’s why not long ago, a second version of the solution was implemented. This time on python, or more precisely on a framework written in python and for python called
So— Nornir this is a micro-framework written in python and for python and intended for automation. Just like in the case of ansible, solving tasks here requires proper data preparation, i.e. inventory of hosts and their parameters, while scripts are not written in a separate DSL, but entirely in that same not very old, but quite good p[и|ай]thon.
Let's take a look at what it is with the following live example.
I have a branch network with several dozen offices across the country. In each office, there is a WAN router that terminates several communication channels from different operators. The routing protocol is BGP. The WAN routers come in two types: Cisco ISG or Juniper SRX.
Acum avem sarcina: trebuie să configurăm pe toate routerele WAN din rețeaua filială o subrețea dedicată pentru supravegherea video pe un port separat — să anunțăm această subrețea în BGP — să configurăm o limitare a vitezei pentru portul dedicat.
La început, trebuie să pregătim un set de șabloane, pe baza cărora vor fi generate configurații separate pentru Cisco și Juniper. De asemenea, trebuie să pregătim date pentru fiecare punct și parametrii de conectare, adică să adunăm acel inventar.
Șablonul final pentru 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Șablonul pentru Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Supraveghere video"
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Șabloanele, desigur, nu sunt luate de nicăieri. Acestea sunt, de fapt, diferentțele între configurațiile de lucru realizate și cele anterioare după soluționarea problemei stabilite pe două routere specifice de modele diferite.
Din șabloanele noastre observăm că pentru a soluționa sarcina avem nevoie de două parametrii pentru Juniper și de 3 parametrii pentru Cisco. Iată-i:
- ifname
- ipsuffix
- asn
Acum trebuie să setăm acești parametrii pentru fiecare dispozitiv, adică să realizăm acel inventar. inventory.
Pentru inventory vom urma clar documentația
adică vom crea aceeași structură de fișiere:
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlFișierul config.yaml — fișierul standard de configurare 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"Parametrii principali îi vom specifica în fișier. hosts.yaml, grupuri (în cazul meu, acestea sunt nume utilizator/parole) în groups.yaml, iar în defaults.yaml nu vom specifica nimic, dar trebuie să introducem trei minusuri – care indică că acesta este yaml fișierul este, deși gol.
Iată cum arată 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: 65111Iată cum arată groups.yaml:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2Așa a ieșit inventory pentru sarcina noastră. La inițializare, parametrii din fișierele inventory sunt mapați pe modelul obiectual InventoryElement.
Sub spoiler se află schema modelului 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"
}
}
}
}
}Acest model poate părea puțin confuz, mai ales la început. Pentru a înțelege mai bine, un mod interactiv în 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 -- 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'
Și, în sfârșit, ajungem la script. Nu am de ce să mă mândresc aici. Pur și simplu am luat un exemplu gata făcut din și l-am folosit aproape fără modificări. Iată cum arată un script working gata:
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)Atenție la parametrul dry_run=True în linia de inițializare a obiectului nr.
Aici, de asemenea, este implementat un test de rulare, în care se conectează la router, se pregătește o nouă configurație modificată, care apoi este validată de dispozitiv (dar nu este sigur; depinde de suportul dispozitivului și de implementarea driverului în NAPALM), dar nu se aplică direct noua configurație. Pentru aplicarea efectivă este necesar să eliminați parametrul ansible dry_run sau să-i schimbați valoarea în Când rulezi scenariul, Nornir oferă loguri detaliate în consolă. False.
Sub spoiler se află ieșirea din rularea efectivă pe două mașini de testare:
Под спойлером вывод боевого прогона на двух тестовых машрутизаторах:
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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Ascundem parolele în ansible_vault
La începutul articolului am fost puțin critic la ansible, dar nu este atât de rău. Îmi place foarte mult la ei vault îți place, care este destinat pentru a ascunde informațiile sensibile de ochii lumii. Și probabil că mulți au observat că toate login-urile/parolele pentru toate ruterele pot fi văzute în clar în fișier. gorups.yaml. Nu arată bine. Să ne protejăm aceste date cu ajutorul vault.
Vom muta parametrii din groups.yaml în creds.yaml și îi vom cripta cu AES256 folosind o parolă de 20 de caractere:
$ 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
Criptarea a avut succes
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435Așa de simplu. Rămâne doar să învățăm scriptul nostru să extragă și să aplice aceste date. Nornir-scriptul pentru a accesa și utiliza aceste date.
Pentru aceasta, în scriptul nostru, după linia de inițializare nr = InitNornir(config_file=… adăugăm următorul cod:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # setează dry_run=False, încrucișează degetele și rulează din nou
# îmbogățește Inventarul cu datele criptate din 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))
# rulează sarcini
...Desigur, vault.passwd nu ar trebui să fie lângă creds.yaml așa cum este în exemplul meu. Dar pentru a experimenta, este suficient.
Asta e tot pentru moment. Urmează încă câteva articole despre Cisco + Zabbix, dar acestea sunt puțin diferite de automatizare. Și în viitorul apropiat, intenționez să scriu despre RESTCONF în Cisco.
Sursa: habr.com
