Automatické generování a plnění prvků konfigurace síťových zařízení pomocí Norniru

Automatické generování a plnění prvků konfigurace síťových zařízení pomocí Norniru

Čau Habr!

Nedávno zde vyskočil článek Mikrotik a Linux. Rutina a automatizace kde byl podobný problém vyřešen pomocí fosilních prostředků. A ačkoliv je úkol zcela typický, na Habrého nic podobného není. Troufám si nabídnout své kolo respektované IT komunitě.

Není to první kolo pro takový úkol. První možnost byla implementována před několika lety ansible verze 1.x.x. Kolo bylo málo používané a proto neustále zrezivělé. V tom smyslu, že samotný úkol nevzniká tak často, jak jsou verze aktualizovány ansible. A pokaždé, když potřebujete jet, spadne řetěz nebo upadne kolo. Nicméně první část, generování konfigurací, funguje vždy velmi přehledně, naštěstí jinja2 Motor je dlouho zavedený. Ale druhá část – zavádění konfigurací – obvykle přinesla překvapení. A protože musím konfiguraci na dálku rozbalit na půl stovce zařízení, z nichž některá se nacházejí tisíce kilometrů daleko, bylo používání tohoto nástroje trochu nudné.

Zde musím přiznat, že moje nejistota s největší pravděpodobností spočívá v mé neznalosti ansiblenež ve svých nedostatcích. A to je mimochodem důležitý bod. ansible je zcela samostatná, vlastní oblast znalostí s vlastním DSL (Domain Specific Language), která musí být udržována na důvěryhodné úrovni. No, ten moment ansible Rozvíjí se poměrně rychle a bez zvláštního zřetele na zpětnou kompatibilitu to na sebevědomí nepřidává.

Není to tak dávno, co byla implementována druhá verze jízdního kola. Tentokrát dál krajta, nebo spíše na frameworku napsaném v krajta a pro krajta oprávněn Nornir

Tak - Nornir je mikrorámec napsaný v krajta a pro krajta a určené pro automatizaci. Stejné jako v případě s ansible, k řešení problémů je zde nutná kompetentní příprava dat, tzn. soupis hostitelů a jejich parametrů, ale skripty jsou psány nikoli v samostatném DSL, ale ve stejném nepříliš starém, ale velmi dobrém p[i|i]tonu.

Podívejme se, co to je, pomocí následujícího živého příkladu.

Mám pobočkovou síť s několika desítkami poboček po celé republice. Každá kancelář má WAN router, který ukončuje několik komunikačních kanálů od různých operátorů. Směrovací protokol je BGP. Směrovače WAN se dodávají ve dvou typech: Cisco ISG nebo Juniper SRX.

Nyní úkol: musíte nakonfigurovat vyhrazenou podsíť pro Video Surveillance na samostatném portu na všech směrovačích WAN pobočkové sítě - inzerujte tuto podsíť v BGP - nastavte rychlostní limit vyhrazeného portu.

Nejprve si musíme připravit pár šablon, na základě kterých se vygenerují konfigurace zvlášť pro Cisco a Juniper. Dále je nutné připravit data pro každý bod a parametry připojení, tzn. sbírat stejný inventář

Připravená šablona pro 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

Šablona pro 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

Šablony samozřejmě nevznikají ze vzduchu. Jedná se v podstatě o rozdíly mezi pracovními konfiguracemi, které byly a byly po vyřešení úlohy na dvou konkrétních routerech různých modelů.

Z našich šablon vidíme, že k vyřešení problému potřebujeme pouze dva parametry pro Juniper a 3 parametry pro Cisco. Zde jsou:

  • ifname
  • přípona ips
  • asn

Nyní musíme tyto parametry nastavit pro každé zařízení, tzn. udělat to samé inventář.

pro inventář Budeme se důsledně řídit dokumentací Inicializace Nornir

to znamená, že vytvoříme stejnou kostru souboru:

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

Soubor config.yaml je standardní konfigurační soubor 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"

Hlavní parametry uvedeme v souboru hosts.yaml, skupina (v mém případě se jedná o přihlašovací údaje/hesla) v skupiny.yamlA defaults.yaml Nebudeme nic naznačovat, ale musíte tam zadat tři mínusy, což znamená, že ano yaml soubor je však prázdný.

Takto vypadá 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 tady jsou groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Tohle se stalo inventář pro náš úkol. Během inicializace jsou parametry ze souborů inventáře mapovány na objektový model InventoryElement.

Pod spoilerem 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 vypadat trochu matoucí, zejména zpočátku. Abychom na to přišli, zapněte interaktivní režim krajta.

 $ 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 nakonec přejděme k samotnému scénáři. Tady nemám být na co zvlášť hrdý. Vzal jsem si jen hotový příklad z tutorial a používal ji téměř beze změny. Takto vypadá 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)

Věnujte pozornost parametru dry_run=Pravda inicializace objektu v řadě nr.
Zde totéž jako v ansible byl implementován zkušební provoz, při kterém se provede připojení k routeru, připraví se nová upravená konfigurace, která je následně validována zařízením (není to však jisté, záleží na podpoře zařízení a implementaci ovladače v NAPALM) , ale nová konfigurace není přímo použita. Pro bojové použití musíte parametr odebrat dry_run nebo změňte jeho hodnotu na Falešný.

Když je skript spuštěn, Nornir odešle podrobné protokoly do konzole.

Pod spoilerem je výstup bojového běhu na dvou testovacích routerech:

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

Skrytí hesel v ansible_vault

Na začátku článku jsem to trochu přehnal ansible, ale není to tak špatné. Mám je moc ráda klenba jako, který je navržen tak, aby skryl citlivé informace mimo dohled. A pravděpodobně si mnozí všimli, že máme všechna přihlášení/hesla pro všechny bojové routery v otevřené podobě v souboru gorups.yaml. Není to hezké, samozřejmě. Chraňme tato data pomocí klenba.

Přeneseme parametry z groups.yaml do creds.yaml a zašifrujeme je pomocí AES256 s 20místným heslem:

$ 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é. Zbývá učit naše Nornir-script k načtení a použití těchto dat.
Chcete-li to provést, v našem skriptu po inicializačním řádku nr = InitNornir(config_file=… přidejte následující 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
...

Vault.passwd by samozřejmě neměl být umístěn vedle creds.yaml jako v mém příkladu. Ale na hraní je to v pořádku.

To je prozatím vše. Přichází několik dalších článků o Cisco + Zabbix, ale toto není tak trochu o automatizaci. A v blízké budoucnosti plánuji psát o RESTCONF v Cisco.

Zdroj: www.habr.com

Přidat komentář