Generarea și completarea automată a elementelor de configurare a dispozitivelor de rețea folosind Nornir

Generarea și completarea automată a elementelor de configurare a dispozitivelor de rețea folosind Nornir

Hei Habr!

Recent, un articol a apărut aici Mikrotik și Linux. Rutină și automatizare unde o problemă similară a fost rezolvată folosind mijloace fosile. Și, deși sarcina este complet tipică, nu există nimic similar la Habré. Îndrăznesc să ofer bicicleta mea respectatei comunități IT.

Aceasta nu este prima bicicletă pentru o astfel de sarcină. Prima opțiune a fost implementată în urmă cu câțiva ani ansible versiunea 1.x.x. Bicicleta a fost folosită rar și, prin urmare, a ruginit constant. În sensul că sarcina în sine nu apare la fel de des pe măsură ce versiunile sunt actualizate ansible. Și de fiecare dată când trebuie să conduceți, lanțul cade sau roata cade. Cu toate acestea, prima parte, care generează configurații, funcționează întotdeauna foarte clar, din fericire jinja2 Motorul este de mult stabilit. Dar a doua parte - lansarea configurațiilor - aducea de obicei surprize. Și din moment ce trebuie să lansez configurația de la distanță pe o jumătate de sută de dispozitive, dintre care unele sunt situate la mii de kilometri distanță, utilizarea acestui instrument a fost puțin plictisitoare.

Aici trebuie să recunosc că incertitudinea mea constă cel mai probabil în lipsa mea de familiaritate cu ansibledecât în ​​lipsurile sale. Și acesta, apropo, este un punct important. ansible este o zonă complet separată, proprie de cunoștințe, cu propriul DSL (Domain Specific Language), care trebuie menținut la un nivel de încredere. Ei bine, în acel moment ansible Se dezvoltă destul de repede și, fără o atenție specială pentru compatibilitatea cu versiunea anterioară, nu adaugă încredere.

Prin urmare, nu cu mult timp în urmă a fost implementată oa doua versiune a bicicletei. De data asta piton, sau mai degrabă pe un cadru scris în piton și pentru piton denumit Nornir

Asa de - Nornir este un microcadru scris în piton și pentru piton și concepute pentru automatizare. La fel ca în cazul cu ansible, pentru rezolvarea problemelor aici este necesară pregătirea competentă a datelor, adică. inventarul de gazde și parametrii acestora, dar scripturile sunt scrise nu într-un DSL separat, ci în același p[i|i]ton nu foarte vechi, dar foarte bun.

Să ne uităm la ce este folosind următorul exemplu live.

Am o rețea de sucursale cu câteva zeci de birouri în toată țara. Fiecare birou are un router WAN care termină mai multe canale de comunicație de la diferiți operatori. Protocolul de rutare este BGP. Routerele WAN vin în două tipuri: Cisco ISG sau Juniper SRX.

Acum sarcina: trebuie să configurați o subrețea dedicată pentru supraveghere video pe un port separat pe toate routerele WAN ale rețelei de filiale - faceți publicitate acestei subrețea în BGP - configurați limita de viteză a portului dedicat.

În primul rând, trebuie să pregătim câteva șabloane, pe baza cărora configurațiile vor fi generate separat pentru Cisco și Juniper. De asemenea, este necesar să se pregătească date pentru fiecare punct și parametrii de conexiune, de ex. colectează același inventar

Șablon gata 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

Șablon pentru ienupăr:

$ 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

Șabloanele, desigur, nu ies din aer. Acestea sunt în esență diferențe între configurațiile de lucru care au fost și au fost după rezolvarea sarcinii pe două routere specifice de modele diferite.

Din șabloanele noastre vedem că pentru a rezolva problema, avem nevoie doar de doi parametri pentru Juniper și 3 parametri pentru Cisco. aici sunt ei:

  • ifname
  • ipsufix
  • ASN

Acum trebuie să setăm acești parametri pentru fiecare dispozitiv, adică. face acelasi lucru inventar.

Pentru inventar Vom respecta cu strictețe documentația Inițializarea Nornir

adică să creăm același schelet de fișiere:

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

Fișierul config.yaml este 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"

Vom indica parametrii principali din fișier gazde.yaml, grup (în cazul meu acestea sunt login/parole) în grupuri.yaml, și în implicite.yaml Nu vom indica nimic, dar trebuie să introduceți trei minusuri acolo - indicând că este yaml fisierul este insa 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: 65111

Și iată groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Asta s-a intamplat inventar pentru sarcina noastră. În timpul inițializării, parametrii din fișierele de inventar sunt mapați la modelul obiect InventoryElement.

Sub spoiler este o diagramă a 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, modul interactiv intră piton.

 $ 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, să trecem la scenariul în sine. Nu am nimic de care să fiu deosebit de mândru aici. Am luat doar un exemplu gata făcut de la tutorial și l-a folosit aproape neschimbat. Iată cum arată scriptul de lucru finalizat:

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)

Acordați atenție parametrului dry_run=Adevărat inițializarea obiectului în linie nr.
Aici la fel ca în ansible a fost implementată o rulare de probă în care se realizează o conexiune la router, se pregătește o nouă configurație modificată, care este apoi validată de dispozitiv (dar acest lucru nu este sigur; depinde de suportul dispozitivului și de implementarea driverului în NAPALM) , dar noua configurație nu este aplicată direct. Pentru utilizare în luptă, trebuie să eliminați parametrul dry_run sau modificați valoarea în Fals.

Când scriptul este executat, Nornir trimite jurnalele detaliate în consolă.

Sub spoiler este rezultatul unei rulări de luptă pe două routere 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ascunderea parolelor în ansible_vault

La începutul articolului am trecut puțin peste bord ansible, dar nu e chiar atât de rău. chiar imi place de ei seif cum ar fi, care este conceput pentru a ascunde informațiile sensibile din vedere. Și probabil mulți au observat că avem toate login-urile/parolele pentru toate routerele de luptă strălucitoare în formă deschisă într-un fișier gorups.yaml. Nu e frumos, desigur. Să protejăm aceste date cu seif.

Să transferăm parametrii de la groups.yaml la creds.yaml și să-i criptăm cu AES256 cu o parolă de 20 de cifre:

$ 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

Este atat de simplu. Rămâne să ne învățăm Nornir-script pentru a prelua și aplica aceste date.
Pentru a face acest lucru, în scriptul nostru după linia de inițializare nr = InitNornir(config_file=… adauga urmatorul cod:

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

Desigur, vault.passwd nu ar trebui să fie situat lângă creds.yaml, ca în exemplul meu. Dar e ok pentru joc.

Asta este tot pentru acum. Mai urmează câteva articole despre Cisco + Zabbix, dar nu este vorba despre automatizare. Și în viitorul apropiat plănuiesc să scriu despre RESTCONF în Cisco.

Sursa: www.habr.com

Adauga un comentariu