Automatinis tinklo įrenginių konfigūracijos elementų generavimas ir pildymas naudojant Nornir

Automatinis tinklo įrenginių konfigūracijos elementų generavimas ir pildymas naudojant Nornir

Sveiki, Habr!

Neseniai čia pasirodė straipsnis Mikrotik ir Linux. Rutina ir automatika kur panaši problema buvo išspręsta naudojant iškastines priemones. Ir nors užduotis yra visiškai tipiška, Habré joje nėra nieko panašaus. Drįstu pasiūlyti savo dviratį gerbiamai IT bendruomenei.

Tai ne pirmas dviratis tokiai užduočiai. Pirmasis variantas buvo įgyvendintas prieš keletą metų įmanoma 1.x.x versija. Dviratis buvo mažai naudotas, todėl nuolat rūdijo. Ta prasme, kad pati užduotis nekyla taip dažnai, kaip atnaujinamos versijos įmanoma. Ir kiekvieną kartą, kai reikia važiuoti, nukrenta grandinė arba nukrenta ratas. Tačiau pirmoji dalis, generuojanti konfigūracijas, visada, laimei, veikia labai aiškiai jinja2 Variklis seniai nusistovėjęs. Tačiau antroji dalis – konfigūracijų išleidimas – dažniausiai atnešdavo staigmenų. Ir kadangi aš turiu nuotoliniu būdu įdiegti konfigūraciją pusšimčiui įrenginių, kai kurie iš jų yra už tūkstančių kilometrų, naudoti šį įrankį buvo šiek tiek nuobodu.

Čia turiu pripažinti, kad mano netikrumas greičiausiai slypi dėl to, kad nesu gerai susipažinęs įmanomanei savo trūkumais. Ir tai, beje, yra svarbus dalykas. įmanoma yra visiškai atskira, savo žinių sritis su savo DSL (domeno specifine kalba), kuri turi būti palaikoma pasitikėjimo lygiu. Na, tą akimirką įmanoma Jis vystosi gana greitai ir, neatsižvelgiant į atgalinį suderinamumą, neprideda pasitikėjimo.

Todėl ne taip seniai buvo įdiegta antroji dviračio versija. Šį kartą toliau pitonas, o tiksliau ant karkaso, parašyto pitonas ir už pitonas vadinamas Nornir

Taigi - Nornir yra mikrokarkasas, parašytas pitonas ir už pitonas ir skirtas automatizavimui. Tas pats kaip ir tuo atveju su įmanoma, norint čia išspręsti problemas, reikalingas kompetentingas duomenų paruošimas, t.y. hostų ir jų parametrų inventorius, bet scenarijai rašomi ne atskiru DSL, o tuo pačiu nelabai senu, bet labai geru p[i|i]tonu.

Pažiūrėkime, kas tai yra, naudodamiesi šiuo tiesioginiu pavyzdžiu.

Turiu filialų tinklą su keliomis dešimtimis biurų visoje šalyje. Kiekviename biure yra WAN maršrutizatorius, kuris nutraukia kelis skirtingų operatorių ryšio kanalus. Maršruto parinkimo protokolas yra BGP. WAN maršrutizatoriai būna dviejų tipų: Cisco ISG arba Juniper SRX.

Dabar užduotis: reikia sukonfigūruoti specialų vaizdo stebėjimo potinklį atskirame prievade visuose filialų tinklo WAN maršrutizatoriuose - reklamuokite šį potinklį BGP - sukonfigūruokite tam skirto prievado greičio apribojimą.

Pirmiausia turime paruošti porą šablonų, kurių pagrindu bus generuojamos atskirai Cisco ir Juniper konfigūracijos. Taip pat reikia paruošti kiekvieno taško duomenis ir prijungimo parametrus, t.y. surinkti tą patį inventorių

Paruoštas Cisco šablonas:

$ 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

Kadagio šablonas:

$ 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

Šablonai, žinoma, neatsiranda iš oro. Tai iš esmės yra skirtumai tarp darbo konfigūracijų, kurios buvo ir buvo išsprendus užduotį dviejuose konkrečiuose skirtingų modelių maršrutizatoriuose.

Iš mūsų šablonų matome, kad norint išspręsti problemą, mums reikia tik dviejų „Juniper“ ir 3 „Cisco“ parametrų. jie yra čia:

  • ifvardas
  • ipsfiksas
  • asn

Dabar turime nustatyti šiuos parametrus kiekvienam įrenginiui, t.y. daryti tą patį inventorius.

inventorius Mes griežtai laikysimės dokumentų Nornir inicijavimas

tai yra, sukurkime tą patį failo skeletą:

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

Config.yaml failas yra standartinis nornir konfigūracijos failas

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

Pagrindinius parametrus nurodysime faile šeimininkai.yaml, grupėje (mano atveju tai yra prisijungimai / slaptažodžiai). grupės.yamlIr defaults.yaml Nieko nenurodysime, bet ten reikia įvesti tris minusus – nurodant, kad taip yaml nors failas tuščias.

Štai kaip atrodo 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

O štai group.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Taip atsitiko inventorius mūsų užduočiai. Inicijavimo metu parametrai iš inventoriaus failų susiejami su objekto modeliu InventoriusElementas.

Po spoileriu yra InventoryElement modelio schema

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

Šis modelis gali atrodyti šiek tiek klaidinantis, ypač iš pradžių. Norėdami tai išsiaiškinti, įjunkite interaktyvųjį režimą pitonas.

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

Ir galiausiai, pereikime prie paties scenarijaus. Čia neturiu kuo ypatingai didžiuotis. Aš tiesiog paėmiau paruoštą pavyzdį iš pamoka ir naudojo beveik nepakitusią. Štai kaip atrodo baigtas darbo scenarijus:

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)

Atkreipkite dėmesį į parametrą dry_run=Tiesa eilutėje objekto inicijavimas nr.
Čia tas pats kaip ir įmanoma buvo atliktas bandomasis paleidimas, kurio metu prisijungiama prie maršrutizatoriaus, paruošiama nauja modifikuota konfigūracija, kurią vėliau patvirtina įrenginys (bet tai nėra aišku; tai priklauso nuo įrenginio palaikymo ir tvarkyklės diegimo NAPALM) , bet nauja konfigūracija nėra tiesiogiai taikoma. Koviniam naudojimui turite pašalinti parametrą sausas_važiavimas arba pakeiskite jo vertę į Klaidingas.

Kai scenarijus vykdomas, Nornir išveda išsamius žurnalus į konsolę.

Po spoileriu yra dviejuose bandomuosiuose maršrutizatoriuose atlikto kovos rezultatas:

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

Slaptažodžių slėpimas ansible_vault

Straipsnio pradžioje šiek tiek persistengiau įmanoma, bet ne viskas taip blogai. Man jie labai patinka skliautas kaip, kuri skirta paslėpti slaptą informaciją iš akių. Ir tikriausiai daugelis pastebėjo, kad mes turime visus prisijungimo vardus / slaptažodžius visiems koviniams maršrutizatoriams, kurie kibirkščiuoja atvira forma faile gorups.yaml. Tai, žinoma, nėra gražu. Apsaugokime šiuos duomenis skliautas.

Perkelkime parametrus iš groups.yaml į creds.yaml ir užšifruokime AES256 su 20 skaitmenų slaptažodžiu:

$ 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

Tai taip paprasta. Belieka išmokyti mūsų Nornir-skriptas šiems duomenims nuskaityti ir pritaikyti.
Norėdami tai padaryti, mūsų scenarijuje po inicijavimo eilutės nr = InitNornir(config_file=… pridėkite šį kodą:

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

Žinoma, vault.passwd neturėtų būti šalia creds.yaml, kaip mano pavyzdyje. Bet žaisti tinka.

Tai kol kas viskas. Bus dar keli straipsniai apie „Cisco + Zabbix“, bet tai ne apie automatizavimą. Ir artimiausiu metu planuoju parašyti apie RESTCONF Cisco.

Šaltinis: www.habr.com

Добавить комментарий