Hálózati eszközök konfigurációs elemeinek automatikus generálása és betöltése Nornir segítségével

Hálózati eszközök konfigurációs elemeinek automatikus generálása és betöltése Nornir segítségével

Helló, Habr!

Nemrégiben megjelent egy cikk itt Mikrotik és Linux. Rutinszerű feladatok és automatizálás ahol hasonló problémákat fosszilis eszközökkel oldottak meg. És bár a feladat teljesen tipikus, a Habr-on nem található róla semmi hasonló. Engedjétek meg, hogy javasoljak a tisztelt IT közösségnek egy saját megoldást.

Ez nem az első megoldás hasonló problémára. Az első verziót néhány éve valósították meg ansible 1.x.x verzióban. A megoldás ritkán használták, ezért folyamatosan rozsdásodott. Abban az értelemben, hogy a feladat nem merül fel olyan gyakran, mint ahogy a verziók frissülnek ansible. És minden alkalommal, amikor használnom kellett, vagy a lánc leesett, vagy a kerék eltört. Az első rész, a konfigurációk generálása azonban mindig nagyon pontosan működött, hála jinja2 a jól bevált motorának. A második rész - a konfigurációk terjesztése - azonban rendszerint meglepetéseket hozott. Mivel a konfigurációkat távolról kell terjesztenem körülbelül ötven eszközön, amelyek közül néhány ezer kilométerre található, ezért abból az eszközből kissé félelmetes volt használni.

Itt el kell ismernem, hogy a bizonytalanságom valójában inkább a ansiblekellemetlen ismeretlenségemből fakad, mint a hibáiból. Ez egy fontos pont. ansible Ez egy teljesen különálló, saját tudás terület, a saját DSL (Domain Specific Language) nyelvével, amelyet biztosan kezelni kell. És az a tény, hogy ansible meglehetősen gyorsan fejlődik, ráadásul nem nagyon figyel a visszafelé való kompatibilitásra, nem növeli a bizalmat.

Ezért nem olyan régen készült el a második verzió. Ezúttal a pythonkeretrendszeren, amelyet a python és a python Nornir néven hívtak Ez tehát -

egy mikrof framework, amelyet a Ez tehát - nyelven írtak és automatizálásra készült. Ahogyan a python és a python -ban is, itt is szükséges a megfelelő adat előkészítés, vagyis a hosztok és jellemzőik nyilvántartása, míg a forgatókönyvek nem külön DSL nyelven készülnek, hanem ugyanazon nem túl régi, de nagyon barátságos P[í|y]tónon. ansibleNézzük meg, miről is van szó egy következő élő példán.

Nézzük meg, miről is van szó a következő élő példán.

Van egy fiókhálózatom, amely több tucat irodát foglal magában az ország egész területén. Minden irodában található egy WAN-router, amely több kapcsolati csatornát terminál a különböző szolgáltatóktól. A forgalomirányítási protokoll BGP. A WAN-routerek két típusúak lehetnek: Cisco ISG vagy Juniper SRX.

A feladat most az, hogy minden WAN-routeren be kell állítani egy dedikált alhálózatot a videomegfigyelés céljára egy külön porton — ezt az alhálózatot be kell hirdetni a BGP-be — be kell állítani a dedikált port sebességkorlátozását.

Először is fel kell készülnünk egy pár sablonra, amelyek alapján külön konfigurációkat fogunk generálni a Cisco és Juniper esetében. Továbbá szükséges adatokat gyűjteni minden pontból és a csatlakozási paramétereket, azaz össze kell gyűjteni a szükséges leltárt.

Kész sablon Cisco számára:

$ 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

Sablon Juniper számára:

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

A sablonok természetesen nem a levegőből jönnek. Ezek alapvetően a két különböző modellű router esetében a megoldás után létrejött munkaképes konfigurációk közötti eltérések.

A sablonjainkból látható, hogy a feladat megoldásához elegendő két paraméter Juniperhez és három paraméter Ciscohoz. Ezek a következők:

  • ifname
  • ipsuffix
  • asn

Most be kell állítanunk ezeket a paramétereket minden eszközhöz, azaz el kell végezni a szükséges lépéseket. inventory.

A inventory Pontosan követjük a dokumentációt. Initializing Nornir

Tehát létrehozunk egy hasonló fájlszerkezetet:

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

A config.yaml fájl - a nornir alapértelmezett konfigurációs fájlja

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

A főbb paramétereket a fájlban fogjuk megadni hosts.yaml, a csoporti (az én esetemben felhasználónév/jelszó) pedig a groups.yaml, míg a defaults.yaml nem tartalmaz semmit, de három kötőjelet kell beírni, amelyek jelzik, hogy ez egy yaml fájl, bármennyire üres is.

Ez így néz ki 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

Így néz ki a groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Ez így alakult inventory a feladatunkhoz. Az inicializáláskor a paraméterek az inventory fájlokból leképeződnek az objektum modellre InventoryElement.

A spoiler alatt található az InventoryElement modell sémája

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

Ez a modell kicsit zavarosnak tűnhet, különösen az elején. A megértéshez nagyon hasznos az interaktív üzemmód a ipython.

 $ ipython3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
Írja be a 'copyright', 'credits' vagy 'license' parancsokat további információkért
IPython 7.1.1 -- Egy továbbfejlesztett interaktív Python. Írja be a '?' a súgóhoz.

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'

Most végre rátérünk magára a szkriptre. Itt igazából nincs mivel dicsekednem. Csak vettem egy kész példát a tutoriálból és szinte változtatás nélkül használtam fel. Itt van, hogyan néz ki a működő szkript:

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):
    # Az inventory adatokat konfigurációvá alakítjuk egy sablonfájl segítségével
    r = task.run(task=text.template_file,
                 name="Alap konfiguráció",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Mentjük a generált konfigurációt egy host változóba
    task.host["config"] = r.result

    # Mentjük a generált konfigurációt egy fájlba
    with open(f"configs/{task.host.hostname}", "w") as f:
        f.write(r.result)

    # Alkalmazzuk azt a konfigurációt az eszközön a NAPALM használatával
    task.run(task=networking.napalm_configure,
             name="Konfiguráció betöltése az eszközre",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # állítsuk be a dry_run értékét False-ra, keresztbe feszítjük az ujjainkat és újra futtatjuk

# feladatok futtatása
result = nr.run(task=config_and_deploy)
print_result(result)

Felhívjuk a figyelmet a dry_run=True paraméterre az objektum inicializálásánál nr.
Itt is, mint a ansible tesztfutásra került sor, ahol kapcsolat létesül az útválasztóval, előkészítjük az új, módosított konfigurációt, amelyet az eszköz érvényesít (de ez nem biztos; függ az eszköz támogatásától és a NAPALM-hoz tartozó meghajtó implementálásáról), de a módosított konfiguráció tényleges alkalmazása nem történik meg. Éles használathoz el kell távolítani a dry_run paramétert, vagy meg kell változtatni az értékét False.

A Nornir szkript futtatásakor részletes naplókat ad a konzolra.

A spoiler alatt két teszt útválasztón végrehajtott élő futás kimenete található:

config_and_deploy***************************************************************
* cisco-test ** megváltoztatva: Igen *******************************************
vvvv config_and_deploy ** megváltoztatva: Igen vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Alap konfiguráció ** megváltoztatva: Igen ------------------------------------- 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
---- Konfiguráció betöltése az eszközre ** megváltoztatva: Igen --------------------- 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
^^^^ VÉGE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** megváltoztatva: Igen *******************************************
vvvv config_and_deploy ** megváltoztatva: Igen vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Alap konfiguráció ** megváltoztatva: Igen ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Videó megfigyelés"
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
---- Konfiguráció betöltése az eszközre ** megváltoztatva: Igen --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "Videó megfigyelés";
+           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;
+              }
+          }
+      }
+  }
^^^^ VÉGE config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Tartsuk titokban a jelszavakat az ansible_vault-ban

A cikk elején kicsit kritizáltam ansible, de ott nem minden olyan rossz. Nagyon tetszett náluk vault Ami titokban tart érzékeny információkat a nyilvánosság elől. Valószínűleg sokan észrevették, hogy a bejelentkezési adatok/jelszavak minden üzleti routerünknél nyíltan villognak egy fájlban. gorups.yaml. Ez persze nem szép. Védjük meg ezeket az adatokat a vault.

Az adatokat átviszsky a groups.yaml-ból a creds.yaml-ba, és AES256-tal titkosítjuk 20 karakteres jelszóval:

$ 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  
Titkosítás sikeres
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

Így egyszerű. Most tanítsuk meg a Ez tehát --szkriptnek kinyerni és alkalmazni ezeket az adatokat.
Ehhez a szkriptünkben az inicializáló sor után nr = InitNornir(config_file=… hozzáadjuk a következő kódot:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # állítsd dry_run=False-ra, bízzunk benne és futtassuk újra

# gazdagítjuk az Inventory-t a titkosított vault adatokkal
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))

# futtassuk a feladatokat
...

Természetesen a vault.passwd nem állhat közvetlenül a creds.yaml mellett, mint az én példámban. De a kipróbáláshoz megfelel.

Ennyit mára. Két új cikk közeleg a Cisco + Zabbix-ról, de az nem teljesen az automatizálásról szól. A közeljövőben szeretnék írni a RESTCONF-ról a Cisco-nál.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster