
Hallo, Habr!
Onlangs verscheen er een artikel hier waarbij een soortgelijke taak met verouderde middelen werd opgelost. En hoewel het een volledig typische taak is, is er op Habr hierover niets dergelijks te vinden. Ik waag het om de gerespecteerde IT-gemeenschap mijn fiets voor te stellen.
Dit is niet de eerste fiets voor een dergelijke taak. De eerste variant werd enkele jaren geleden nog op ansible versie 1.x.x geïmplementeerd. De fiets werd zelden gebruikt en roestte daardoor constant. In die zin komt de taak niet zo vaak voor als de versies worden bijgewerkt ansible. En elke keer als ik moet gaan, valt de ketting af of valt het wiel eraf. De eerste fase, het genereren van configuraties, werkt gelukkig altijd heel nauwkeurig, aangezien jinja2 de engine al lang een gevestigde waarde is. Maar de tweede fase - het uitrollen van configuraties, leverde meestal verrassingen. En aangezien ik configuraties op afstand moet uitrollen naar een vijftigtal apparaten, waarvan sommige duizenden kilometers ver weg zijn, was het gebruik van dit instrument een beetje eng.
Hier moet ik toegeven dat mijn onzekerheid eerder ligt in mijn gebrek aan vertrouwdheid met ansible, dan in zijn tekortkomingen. En dit is trouwens een belangrijk punt. ansible - dit is een volledig apart, eigen kennisgebied met een eigen DSL (Domain Specific Language), dat op een betrouwbaar niveau moet worden onderhouden. En het feit dat ansible snel genoeg ontwikkelt, zonder veel aandacht voor backwards compatibility, maakt het er niet beter op.
Daarom werd niet zo lang geleden een tweede versie van de fiets geïmplementeerd. Dit keer op python, of liever op een framework geschreven in python en voor python StopMovementInTheNextTick
Dus - Nornir dit is een microframework geschreven in python en voor python en bedoeld voor automatisering. Net als in het geval van ansible, vereist het bij het oplossen van taken hier een correcte voorbereidingen van gegevens, dwz. inventarisatie van hosts en hun parameters, en de scripts worden niet in een aparte DSL geschreven, maar allemaal in dezelfde niet zo oude en zeer goede p[и|ай]ton.
Laten we eens kijken wat het is aan de volgende live voorbeeld.
Ik heb een filiaalnetwerk met enkele tientallen kantoren verspreid over het land. In elk kantoor is er een WAN-router die verschillende communicatiekanalen van verschillende providers beëindigt. Het routeringsprotocol is BGP. WAN-routers zijn er in twee types: Cisco ISG of Juniper SRX.
De taak is nu: op alle WAN-routers van het filiaalnetwerk een aparte subnet voor video-observatie te configureren op een aparte poort — deze subnet in BGP aan te kondigen — en de snelheidslimiet voor de aparte poort in te stellen.
Eerst moeten we een paar sjablonen voorbereiden, op basis waarvan de configuraties afzonderlijk voor Cisco en Juniper zullen worden gegenereerd. Ook moeten we gegevens over elk punt voorbereiden en de verbindingsparameters, d.w.z. het inventaris verzamelen.
Klaar sjabloon voor 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 anySjabloon voor Juniper:
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Videobewaking"
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 limiterDe sjablonen worden natuurlijk niet uit de lucht gegrepen. Dit zijn in wezen de verschillen tussen de werkconfiguraties voor en na het oplossen van de taak op twee specifieke routers van verschillende modellen.
Uit onze sjablonen zien we dat we voor het oplossen van de taak genoeg hebben aan twee parameters voor Juniper en drie parameters voor Cisco. Dit zijn ze:
- ifname
- ipsuffix
- asn
Nu moeten we deze parameters voor elk apparaat instellen, d.w.z. het inventaris doen. inventaris.
Voor inventaris we zullen strikt de documentatie volgen
d.w.z. we creëren zo'n zelfde bestandsstructuur:
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlHet bestand config.yaml is het standaard configuratiebestand voor 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 belangrijkste parameters worden in het bestand opgegeven hosts.yaml, groepen (in mijn geval zijn dit logins/wachtwoorden) in groups.yaml, en in defaults.yaml we laten niets achter, maar het is noodzakelijk om drie streepjes in te vullen — die aangeven dat dit yaml het bestand is, hoewel het leeg is.
Zo ziet hosts.yaml eruit:
---
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: 65111En zo ziet groups.yaml eruit:
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2Dit is wat eruit kwam inventaris voor onze taak. Bij de initialisatie worden de parameters uit de inventory-bestanden toegewezen aan het objectmodel InventoryElement.
Onder de spoiler het schema van het InventoryElement-model
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"
}
}
}
}
}Dit model kan in het begin een beetje verwarrend lijken. Het gebruik van de interactieve modus helpt enorm om het te begrijpen. ipython.
$ ipython3
Python 3.6.9 (standaard, 7 nov 2019, 10:44:02)
Typ 'copyright', 'credits' of 'license' voor meer informatie
IPython 7.1.1 -- Een verbeterde interactieve Python. Typ '?' voor hulp.
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'
En tot slot komen we bij het script zelf. Hier is er niet veel waar ik trots op kan zijn. Ik heb gewoon een kant-en-klaar voorbeeld genomen uit en het bijna zonder wijzigingen gebruikt. Zo ziet het voltooide werkende script eruit:
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):
# Transformeer inventarisdata naar configuratie via een sjabloonbestand
r = task.run(task=text.template_file,
name="Basisconfiguratie",
template="base.j2",
path=f"templates/{task.host.platform}")
# Sla de gecompileerde configuratie op in een hostvariabele
task.host["config"] = r.result
# Sla de gecompileerde configuratie op in een bestand
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Implementeer die configuratie op het apparaat met NAPALM
task.run(task=networking.napalm_configure,
name="Configuratie laden op het apparaat",
replace=False,
configuration=task.host["config"])
nr = InitNornir(config_file="config.yaml", dry_run=True) # zet dry_run=False, kruis je vingers en voer opnieuw uit
# voer taken uit
result = nr.run(task=config_and_deploy)
print_result(result)Let op het parameter dry_run=True in de regel van objectinitialisatie nr.
Hier is het net zoals bij de ansible uitgevoerd testloop waarin verbinding wordt gemaakt met de router, een nieuwe gewijzigde configuratie wordt voorbereid, die vervolgens door het apparaat wordt gevalideerd (maar dat is niet zeker; hangt af van de ondersteuning door het apparaat en de implementatie van de driver in NAPALM), maar de nieuwe configuratie wordt niet daadwerkelijk toegepast. Voor de productieimplementatie moet de parameter dry_run of zijn waarde worden gewijzigd in False.
Bij het uitvoeren van het Nornir-script geeft de console gedetailleerde logboeken weer.
Onder de spoiler is de uitvoer van de productie-run op twee testrouters:
config_and_deploy***************************************************************
* cisco-test ** gewijzigd : Waar *******************************************
vvvv config_and_deploy ** gewijzigd : Waar vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basisconfiguratie ** gewijzigd : Waar ------------------------------------- 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
beschrijving VIDEOSURV
ip adres 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
---- Configuratie laden op het apparaat ** gewijzigd : Waar --------------------- INFO
+class-map match-all VIDEO_SURV
+ match access-group 111
+policy-map VIDEO_SURV
+ class VIDEO_SURV
+interface GigabitEthernet0/1/1
+ beschrijving VIDEOSURV
+ ip adres 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
^^^^ EINDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** gewijzigd : Waar *******************************************
vvvv config_and_deploy ** gewijzigd : Waar vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Basisconfiguratie ** gewijzigd : Waar ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 beschrijving "Videobewaking"
set interfaces fe-0/0/2 unit 0 family inet filter input limit-in
set interfaces fe-0/0/2 unit 0 family inet adres 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
---- Configuratie laden op het apparaat ** gewijzigd : Waar --------------------- INFO
[edit interfaces]
+ fe-0/0/2 {
+ unit 0 {
+ beschrijving "Videobewaking";
+ family inet {
+ filter {
+ input limit-in;
+ }
+ adres 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;
+ }
+ }
+ }
+ }
^^^^ EINDE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Verberg wachtwoorden in ansible_vault
Aan het begin van het artikel heb ik iets negatiefs gezegd over ansible, maar het is niet allemaal slecht. Ik vind ze erg vault Het is fijn dat we de gevoelige informatie uit het zicht kunnen verbergen. En waarschijnlijk hebben velen opgemerkt dat al onze inloggegevens voor alle actieve routers openlijk in een bestand worden weergegeven. gorups.yaml. Dit is natuurlijk niet mooi. Laten we deze gegevens beveiligen met behulp van vault.
Laten we de parameters uit groups.yaml naar creds.yaml verplaatsen en deze AES256 versleutelen met een wachtwoord van 20 tekens:
$ 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
Versleuteling geslaagd
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435Zo eenvoudig is het. Nu moeten we onze Nornir-script leren om deze gegevens op te halen en toe te passen.
Hiervoor voegen we in ons script na de initialisatieregel nr = InitNornir(config_file=… de volgende code toe:
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, hoop op het beste en voer opnieuw uit
# Verrijk de inventaris met de versleutelde kluisgegevens
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))
# voer taken uit
...Natuurlijk mag vault.passwd niet naast creds.yaml liggen, zoals in mijn voorbeeld. Maar het is goed genoeg om mee te spelen.
Dat is het voor nu. Er komen nog een paar artikelen aan over Cisco + Zabbix, maar dat is niet echt over automatisering. In de nabije toekomst ben ik van plan om over RESTCONF in Cisco te schrijven.
Bron: habr.com
