
Hello, Habr!
For nylig så jeg en artikel hvor en lignende opgave blev løst med forhistoriske midler. Og selvom opgaven er helt typisk, kan jeg ikke finde noget lignende om den på Hacker News. Jeg vil tillade mig at foreslå mit eget lille cykel til det respektable IT-samfund.
Det er ikke den første cykel til en sådan opgave. Den første version blev implementeret for flere år siden på ansible version 1.x.x. Cyklen blev sjældent brugt, og derfor røg den konstant til. ansibleDet vil sige, at selve opgaven ikke opstår så ofte som versionerne opdateres. jinja2 Og hver gang jeg skulle bruge den, faldt kæden af, eller hjulet faldt af. Dog fungerede den første del, genereringen af configs, altid meget præcist, godt
den motor, der længe er blevet etableret. Men den anden del - rullende configs, bragte som regel overraskelser. Og eftersom jeg skal rulle config ud fjernbetjent på et halvt hundrede enheder, hvoraf nogle befinder sig tusinder af kilometer væk, var det lidt skræmmende at bruge dette værktøj. ansibleHer må jeg indrømme, at min usikkerhed snarere skyldes min utilstrækkelige bekendtskab med ansible , end med dets mangler. Og det er i øvrigt et vigtigt punkt. ansible - det er et helt separat, selvstændigt vidensområde med sit eget DSL (Domain Specific Language), som man skal opretholde på et sikkert niveau. Og det faktum, at
udvikler sig relativt hurtigt, uden at tænke særligt meget på bagudkompatibilitet, giver ikke meget tillid. pythonDerfor blev der for ikke længe siden implementeret en anden variant af cyklen. Denne gang på python og for python med navnet
Nornir , nærmere bestemt rammeværket skrevet på Så - python og for python det er et mikro-rammeværk, der er skrevet på ansibleog beregnet til automatisering. Ligesom i tilfælde med
, kræver løsningen af opgaver her fornuftig dataklargøring, dvs. inventarisering af værter og deres parametre, mens scenarierne skrives ikke i et separat DSL, men alle i det samme ikke så gamle, men meget venlige p[и|ай]ton.
Lad os se nærmere på, hvad det er i et næste levende eksempel. Jeg har et filialnetværk med flere ti kontorer rundt om i landet. I hvert kontor findes der en WAN-router, der terminerer flere kommunikationskanaler fra forskellige operatører. Routingprotokollen er BGP. WAN-rutere findes i to typer: Cisco ISG eller Juniper SRX.
Opgaven er nu: vi skal konfigurere et dedikeret subnet til videoovervågning på alle WAN-routere i filialnetværket i en separat port — annoncer dette subnet i BGP — konfigurere hastighedsbegrænsningen for den dedikerede port.
Først skal vi forberede et par skabeloner, som konfigurationer vil blive genereret ud fra separat for Cisco og Juniper. Vi skal også forberede data for hver punkt og tilslutningsparametre, dvs. samle det nødvendige inventory.
Færdig skabelon til 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 anySkabelon til Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Videoovervågning"
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 limiterSkabelonerne kommer selvfølgelig ikke ud af det blå. Det er i bund og grund diff-er mellem de arbejdsmæssige konfigurationer før og efter løsningen af den stillede opgave på to specifikke routere af forskellige modeller.
Ud fra vores skabeloner ser vi, at vi til at løse opgaven kun har brug for to parametre for Juniper og tre parametre for Cisco. Her er de:
- ifname
- ipsuffix
- asn
Nu skal vi angive disse parametre for hver enhed, dvs. gøre det nødvendige. inventory.
For inventory vi vil følge dokumentationen nøje.
dvs. vi opretter et tilsvarende filskellet:
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlFilen config.yaml — den standard konfigurationsfil for 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"De vigtigste parametre angiver vi i filen hosts.yaml, gruppe (i mit tilfælde login/password) i groups.yaml, og der sendes noget til den, vil flytning af denne task til sektionen defaults.yaml vi vil ikke angive noget, men der skal skrives tre minusser – der angiver, at det er yaml filen er dog tom.
Sådan ser hosts.yaml ud:
---
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: 65111Og sådan ser groups.yaml ud:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2Sådan ser det ud inventory til vores opgave. Ved initialisering mappes parametrene fra inventarfilerne til den objektmodel InventoryElement.
Under spoileren diagrammet for modellen 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"
}
}
}
}
}Denne model kan virke lidt forvirrende, især i starten. Det hjælper meget at bruge interaktiv tilstand i ipython.
$ ipython3
Python 3.6.9 (standard, 7. nov 2019, 10:44:02)
Skriv 'copyright', 'credits' eller 'license' for mere information
IPython 7.1.1 -- En forbedret interaktiv Python. Skriv '?' for hjælp.
I [1]: from nornir import InitNornir
I [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)
I [3]: nr.inventory.hosts
Out[3]:
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}
I [4]: nr.inventory.hosts['srx-test'].data
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}
I [5]: nr.inventory.hosts['srx-test']['task_data']
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}
I [6]: nr.inventory.hosts['srx-test'].platform
Out[6]: 'junos'
Og endelig når vi til selve scriptet. Her er jeg ikke meget stolt. Jeg tog bare et færdigt eksempel fra og brugte det næsten uden ændringer. Sådan ser det færdige fungerende script ud:
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):
# Transformere inventar data til konfiguration via en skabelonfil
r = task.run(task=text.template_file,
name="Base Configuration",
template="base.j2",
path=f"templates/{task.host.platform}")
# Gem den kompilerede konfiguration i en værtsvariabel
task.host["config"] = r.result
# Gem den kompilerede konfiguration i en fil
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Udrul den konfiguration til enheden ved hjælp af 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) # sæt dry_run=False, kryds fingre og kør igen
# kør opgaver
result = nr.run(task=config_and_deploy)
print_result(result)Vær opmærksom på parameteren dry_run=True i initialiseringslinjen for objektet nr.
Her er der også testkørsel, hvor der oprettes forbindelse til ruteren, forberedes en ny ændret konfiguration, som derefter valideres af enheden (men det er ikke sikkert; afhænger af enhedens understøttelse og driverens implementering i NAPALM), men den nye konfiguration anvendes ikke direkte. For at anvende den i produktion skal parameteren ansible dry_run fjernes eller ændres til Når Nornir-scriptet køres, genereres detaljerede logs i konsollen. False.
Under spoileren er output fra den faktiske kørsel på to test-rutere:
Under spoiler the output of the combat run on two test routers:
config_and_deploy***************************************************************
* cisco-test ** ændret : True *******************************************
vvvv config_and_deploy ** ændret : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis konfiguration ** ændret : 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
---- Indlæser konfiguration på enheden ** ændret : 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
^^^^ SLUT på config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** ændret : True *******************************************
vvvv config_and_deploy ** ændret : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basis konfiguration ** ændret : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Videoovervågning"
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
---- Indlæser konfiguration på enheden ** ændret : True --------------------- INFO
[edit interfaces]
+ fe-0/0/2 {
+ unit 0 {
+ description "Videoovervågning";
+ 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;
+ }
+ }
+ }
+ }
^^^^ SLUT på config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Skjuler adgangskoder i ansible_vault
I begyndelsen af artiklen gav jeg lidt kritik til ansible, men der er ikke så meget galt. Jeg kan virkelig godt lide deres vault det er rart, som er beregnet til at skjule følsomme oplysninger for øjnene. Og mange har sikkert bemærket, at vores login/passwords til alle produktions-routere vises åbent i filen gorups.yaml. Det ser ikke pænt ud. Lad os beskytte disse data ved hjælp af vault.
Lad os overføre parametrene fra groups.yaml til creds.yaml, og kryptere det med AES256 med et 20-tegns kodeord:
$ 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
Kryptering lykkedes
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435Så nemt er det. Nu er det tid til at lære vores , nærmere bestemt rammeværket skrevet på-script at hente og anvende disse data.
For at gøre dette tilføjer vi følgende kode i vores script efter initialiseringslinjen nr = InitNornir(config_file=… tilføjer vi følgende kode:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # sæt dry_run=False, kryds fingrene og kør igen
# berige Inventar med de krypterede 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))
# udfør opgaver
...Naturligvis bør vault.passwd ikke ligge ved siden af creds.yaml som i mit eksempel. Men til leg er det fint.
Det er alt for nu. Der kommer et par artikler om Cisco + Zabbix, men det er lidt uden for automatisering. I den nærmeste fremtid planlægger jeg at skrive om RESTCONF i Cisco.
Kilde: habr.com
