Automatické generovanie a vypĺňanie prvkov konfigurácie sieťových zariadení pomocou Norniru

Automatické generovanie a vypĺňanie prvkov konfigurácie sieťových zariadení pomocou Norniru

Čau Habr!

Nedávno tu vyskočil článok Mikrotik a Linux. Rutina a automatizácia kde sa podobný problém riešil pomocou fosílnych prostriedkov. A hoci je úloha úplne typická, na Habrého nič podobné nie je. Dovolím si ponúknuť svoj bicykel rešpektovanej IT komunite.

Toto nie je prvý bicykel na takúto úlohu. Prvá možnosť bola implementovaná pred niekoľkými rokmi ansible verzia 1.x.x. Bicykel bol málo používaný a preto neustále hrdzavý. V tom zmysle, že samotná úloha nevzniká tak často, ako sa aktualizujú verzie ansible. A vždy, keď potrebujete jazdiť, spadne reťaz alebo spadne koleso. Prvá časť, generovanie konfigurácií, však našťastie vždy funguje veľmi prehľadne jinja2 Motor je už dávno zavedený. Ale druhá časť - zavádzanie konfigurácií - zvyčajne priniesla prekvapenia. A keďže musím na diaľku zaviesť konfiguráciu na pol stovke zariadení, z ktorých niektoré sú vzdialené tisíce kilometrov, používanie tohto nástroja bolo trochu nudné.

Tu musím priznať, že moja neistota s najväčšou pravdepodobnosťou spočíva v mojej nedostatočnej znalosti ansiblenež v jeho nedostatkoch. A to je mimochodom dôležitý bod. ansible je úplne samostatná, vlastná oblasť vedomostí s vlastným DSL (Domain Specific Language), ktorý musí byť udržiavaný na dôveryhodnej úrovni. No, ten moment ansible Vyvíja sa pomerne rýchlo a bez osobitného zreteľa na spätnú kompatibilitu to na sebavedomí nepridáva.

Preto nie je to tak dávno, čo bola implementovaná druhá verzia bicykla. Tentoraz na krajta, alebo skôr na framework napísaný v krajta a pre krajta oprávnený Nornir

Takže - Nornir je mikrorámec napísaný v krajta a pre krajta a navrhnuté pre automatizáciu. Rovnako ako v prípade s ansible, na riešenie problémov tu je potrebná kompetentná príprava dát, t.j. inventár hostiteľov a ich parametrov, ale skripty sú písané nie v samostatnom DSL, ale v rovnakom nie veľmi starom, ale veľmi dobrom p[i|i]tóne.

Pozrime sa, čo to je, pomocou nasledujúceho živého príkladu.

Mám pobočkovú sieť s niekoľkými desiatkami kancelárií po celej krajine. Každá kancelária má WAN router, ktorý ukončuje niekoľko komunikačných kanálov od rôznych operátorov. Smerovací protokol je BGP. Smerovače WAN sa dodávajú v dvoch typoch: Cisco ISG alebo Juniper SRX.

Teraz úloha: musíte nakonfigurovať vyhradenú podsieť pre Video Surveillance na samostatnom porte na všetkých smerovačoch WAN pobočkovej siete - inzerovať túto podsieť v BGP - nakonfigurovať rýchlostný limit vyhradeného portu.

Najprv si musíme pripraviť pár šablón, na základe ktorých sa vygenerujú konfigurácie samostatne pre Cisco a Juniper. Taktiež je potrebné pripraviť údaje pre každý bod a parametre pripojenia, t.j. zbierať rovnaký inventár

Pripravená šablóna pre 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

Šablóna pre 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

Šablóny, samozrejme, nevznikajú zo vzduchu. Ide v podstate o rozdiely medzi pracovnými konfiguráciami, ktoré boli a boli po vyriešení úlohy na dvoch konkrétnych smerovačoch rôznych modelov.

Z našich šablón vidíme, že na vyriešenie problému potrebujeme iba dva parametre pre Juniper a 3 parametre pre Cisco. tu sú:

  • ifname
  • ipsufix
  • asn

Teraz potrebujeme nastaviť tieto parametre pre každé zariadenie, t.j. urob to isté inventár.

pre inventár Budeme prísne dodržiavať dokumentáciu Inicializuje sa Nornir

to znamená, že vytvoríme rovnakú kostru súboru:

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

Súbor config.yaml je štandardný konfiguračný súbor 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"

V súbore uvedieme hlavné parametre hosts.yaml, skupina (v mojom prípade sú to prihlasovacie údaje/heslá) v skupiny.yamlA defaults.yaml Nebudeme nič naznačovať, ale musíte tam zadať tri mínusy, čo znamená, že áno yaml súbor je však prázdny.

Takto vyzerá 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

A tu sú groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Toto sa stalo inventár pre našu úlohu. Počas inicializácie sa parametre zo súborov inventára mapujú na objektový model InventoryElement.

Pod spojlerom je schéma modelu 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"
                }
            }
        }
    }
}

Tento model môže vyzerať trochu mätúce, najmä na začiatku. Aby ste na to prišli, interaktívny režim v ipython.

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

A nakoniec prejdime k samotnému scenáru. Nemám tu byť na čo obzvlášť hrdý. Vzal som si len hotový príklad z tutoriál a používal ju takmer nezmenenú. Takto vyzerá hotový pracovný skript:

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)

Venujte pozornosť parametrom dry_run=Pravda riadková inicializácia objektu nr.
Tu to isté ako v ansible bola realizovaná skúšobná prevádzka, pri ktorej sa vytvorí spojenie s routerom, pripraví sa nová upravená konfigurácia, ktorá je následne validovaná zariadením (nie je to však isté, závisí to od podpory zariadenia a implementácie ovládača v NAPALM) , ale nová konfigurácia sa priamo nepoužije. Pre bojové použitie musíte parameter odstrániť dry_run alebo zmeniť jeho hodnotu na Falošný.

Keď sa skript spustí, Nornir odošle podrobné protokoly do konzoly.

Pod spojlerom je výstup z bojovej jazdy na dvoch testovacích smerovačoch:

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

Skrytie hesiel v ansible_vault

Na začiatku článku som to trochu prehnal ansible, ale nie je to až také zlé. Veľmi sa mi páčia klenba ako, ktorý je navrhnutý tak, aby skryl citlivé informácie mimo dohľadu. A pravdepodobne si mnohí všimli, že máme všetky prihlasovacie údaje/heslá pre všetky bojové smerovače v otvorenej podobe v súbore gorups.yaml. Nie je to pekné, samozrejme. Chráňme tieto údaje pomocou klenba.

Prenesme parametre z groups.yaml do creds.yaml a zašifrujeme ich pomocou AES256 s 20-miestnym heslom:

$ 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

Je to také jednoduché. Zostáva poučiť naše Nornir-script na získanie a použitie týchto údajov.
Ak to chcete urobiť, v našom skripte po inicializačnom riadku nr = InitNornir(config_file=… pridajte nasledujúci kód:

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

Samozrejme, vault.passwd by sa nemal nachádzať vedľa creds.yaml ako v mojom príklade. Ale na hranie je to v poriadku.

To je zatiaľ všetko. Prichádza niekoľko ďalších článkov o Cisco + Zabbix, ale toto nie je ani trochu o automatizácii. A v blízkej budúcnosti plánujem písať o RESTCONF v Cisco.

Zdroj: hab.com

Pridať komentár