Automatisch genereren en vullen van configuratie-elementen van netwerkapparaten met behulp van Nornir

Automatisch genereren en vullen van configuratie-elementen van netwerkapparaten met behulp van Nornir

Hé Habr!

Onlangs verscheen hier een artikel Mikrotik en Linux. Routine en automatisering waar een soortgelijk probleem met fossiele middelen werd opgelost. En hoewel de taak volkomen typisch is, is er op Habré niets vergelijkbaars aan. Ik durf mijn fiets aan te bieden aan de gerespecteerde IT-gemeenschap.

Dit is niet de eerste fiets voor een dergelijke taak. De eerste optie werd enkele jaren geleden geïmplementeerd Ansible versie 1.x.x De fiets is zelden gebruikt en daardoor voortdurend verroest. In die zin dat de taak zelf niet zo vaak voorkomt als versies worden bijgewerkt Ansible. En elke keer dat u moet rijden, valt de ketting eraf of valt het wiel eraf. Het eerste deel, het genereren van configuraties, werkt gelukkig altijd heel duidelijk jinja2 De motor bestaat al lang. Maar het tweede deel – het uitrollen van configuraties – bracht meestal verrassingen met zich mee. En aangezien ik de configuratie op afstand moet uitrollen naar een halfhonderd apparaten, waarvan sommige zich duizenden kilometers verderop bevinden, was het gebruik van deze tool een beetje saai.

Hier moet ik toegeven dat mijn onzekerheid hoogstwaarschijnlijk ligt in mijn gebrek aan bekendheid met Ansibledan in zijn tekortkomingen. En dit is trouwens een belangrijk punt. Ansible is een volledig apart, eigen kennisgebied met een eigen DSL (Domain Specific Language), dat op een zelfverzekerd niveau moet worden gehouden. Nou, dat moment dan Ansible Het ontwikkelt zich vrij snel, en zonder speciale aandacht voor achterwaartse compatibiliteit voegt het geen vertrouwen toe.

Daarom werd nog niet zo lang geleden een tweede versie van de fiets geïmplementeerd. Deze keer verder python, of beter gezegd op een raamwerk waarin geschreven python en voor python gerechtigd Nornir

Dus - Nornir is een microframework waarin geschreven is python en voor python en ontworpen voor automatisering. Hetzelfde als in het geval van AnsibleOm de problemen hier op te lossen is een competente gegevensvoorbereiding vereist, d.w.z. inventaris van hosts en hun parameters, maar scripts worden niet in een aparte DSL geschreven, maar in dezelfde niet erg oude, maar zeer goede p[i|i]ton.

Laten we eens kijken naar wat het is met behulp van het volgende live voorbeeld.

Ik heb een vestigingennetwerk met enkele tientallen vestigingen door het hele land. Elk kantoor heeft een WAN-router die verschillende communicatiekanalen van verschillende operators beëindigt. Het routeringsprotocol is BGP. WAN-routers zijn er in twee typen: Cisco ISG of Juniper SRX.

Nu de taak: u moet een speciaal subnet configureren voor videobewaking op een aparte poort op alle WAN-routers van het vestigingsnetwerk - adverteer dit subnet in BGP - configureer de snelheidslimiet van de speciale poort.

Eerst moeten we een aantal sjablonen voorbereiden, op basis waarvan configuraties afzonderlijk voor Cisco en Juniper worden gegenereerd. Het is ook noodzakelijk om gegevens voor elk punt en verbindingsparameters voor te bereiden, d.w.z. verzamel dezelfde inventaris

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 any

Sjabloon voor Juniper:

$ cat templates/junos/base.j2 
set interfaces {{ host.task_data.ifname }} unit 0 description "Video surveillance"
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

Sjablonen komen uiteraard niet uit de lucht vallen. Dit zijn in wezen verschillen tussen de werkende configuraties die waren en waren na het oplossen van de taak op twee specifieke routers van verschillende modellen.

Uit onze sjablonen zien we dat we, om het probleem op te lossen, slechts twee parameters voor Juniper en 3 parameters voor Cisco nodig hebben. daar zijn ze:

  • alsnaam
  • ip-achtervoegsel
  • ASN

Nu moeten we deze parameters voor elk apparaat instellen, d.w.z. hetzelfde doen inventaris.

Voor inventaris Wij zullen de documentatie strikt volgen Nornir initialiseren

dat wil zeggen, laten we hetzelfde bestandsskelet maken:

.
├── config.yaml
├── inventory
│   ├── defaults.yaml
│   ├── groups.yaml
│   └── hosts.yaml

Het bestand config.yaml is het standaard nornir-configuratiebestand

$ 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"

We zullen de belangrijkste parameters in het bestand aangeven hosts.yaml, groep (in mijn geval zijn dit logins/wachtwoorden) in groepen.yamlen in standaardinstellingen.yaml We geven niets aan, maar je moet daar drie minnen invoeren - wat aangeeft dat dit zo is YAML het bestand is echter leeg.

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: 65111

En hier is groups.yaml:

---
cisco:
    platform: ios
    username: admin1
    password: cisco1

juniper:
    platform: junos
    username: admin2
    password: juniper2

Dit is wat er gebeurde inventaris voor onze taak. Tijdens de initialisatie worden parameters uit inventarisbestanden toegewezen aan het objectmodel InventarisElement.

Onder de spoiler staat een diagram 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 er een beetje verwarrend uitzien, vooral in het begin. Om dit uit te zoeken, moet de interactieve modus worden ingeschakeld Python.

 $ 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'

En tot slot gaan we verder met het script zelf. Ik heb hier niets om bijzonder trots op te zijn. Ik heb zojuist een kant-en-klaar voorbeeld genomen zelfstudie en gebruikte het vrijwel onveranderd. Dit is hoe het voltooide werkscript eruit ziet:

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)

Let op de parameter dry_run=Waar bij initialisatie van lijnobjecten nr.
Hier hetzelfde als in Ansible er is een testrun geïmplementeerd waarbij verbinding wordt gemaakt met de router, een nieuwe aangepaste configuratie wordt voorbereid, die vervolgens door het apparaat wordt gevalideerd (maar dit is niet zeker; het hangt af van de apparaatondersteuning en de driverimplementatie in NAPALM) , maar de nieuwe configuratie wordt niet rechtstreeks toegepast. Voor gevechtsgebruik moet u de parameter verwijderen oefening of wijzig de waarde ervan in Niet waar.

Wanneer het script wordt uitgevoerd, voert Nornir gedetailleerde logboeken uit naar de console.

Onder de spoiler staat de uitvoer van een gevechtsrun op twee testrouters:

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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Wachtwoorden verbergen in ansible_vault

Aan het begin van het artikel ging ik een beetje overboord Ansible, maar dat is allemaal niet zo erg. Ik vind ze echt leuk Gewelf zoals, dat is ontworpen om gevoelige informatie uit het zicht te verbergen. En waarschijnlijk hebben velen gemerkt dat we alle logins/wachtwoorden voor alle gevechtsrouters in open vorm in een bestand hebben staan groepen.yaml. Het is natuurlijk niet mooi. Laten we deze gegevens beschermen met Gewelf.

Laten we de parameters van groups.yaml naar creds.yaml overbrengen en deze versleutelen met AES256 met een wachtwoord van 20 cijfers:

$ 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  
Encryption successful
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

Het is zo simpel. Het blijft ons leren Nornir-script om deze gegevens op te halen en toe te passen.
Om dit te doen, in ons script na de initialisatieregel nr = InitNornir(config_bestand=… voeg de volgende code toe:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# enrich Inventory with the encrypted 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))

# run tasks
...

Uiteraard mag kluis.passwd niet naast creds.yaml worden geplaatst, zoals in mijn voorbeeld. Maar het is prima om te spelen.

Dat is het voor nu. Er komen nog een paar artikelen over Cisco + Zabbix, maar dit gaat niet alleen over automatisering. En in de nabije toekomst ben ik van plan om over RESTCONF in Cisco te schrijven.

Bron: www.habr.com

Voeg een reactie