Autogjenerimi dhe ngarkimi i elementeve të konfigurimeve të pajisjeve rrjetore me ndihmën e Nornir

Autogjenerimi dhe ngarkimi i elementeve të konfigurimeve të pajisjeve rrjetore me ndihmën e Nornir

Përshëndetje, Habr!

Së fundmi, këtu kaloi një artikull Mikrotik dhe Linux. Routine dhe automatizim ku një detyrë të tillë e kishin zgjidhur me mjete të vjetra. Edhe pse detyra është krejtësisht tipike, në Habra nuk gjendet asgjë e ngjashme për të. Gujoj të propozoj komunitetit të nderuar IT propozimin tim.

Ky nuk është biçikleta e parë për një detyrë të tillë. Versioni i parë u realizua disa vite më parë në ansible versionin 1.x.x. Biçikleta përdorej rrallë dhe për këtë arsye gjithmonë rjente. Në atë kuptim, detyra vetë nuk shfaqet aq shpesh sa përditësohen versionet ansible. Dhe çdo herë që duhet të procedoj, zinxhiri bie, ose rrota shkëputet. Megjithatë, pjesa e parë, gjenerimi i konfigurimeve, gjithmonë funksionon shumë saktë, falë jinja2 motorit që është stabilizuar prej kohësh. Por pjesa e dytë — implementimi i konfigurimeve zakonisht ka sjellë surpriza. Dhe për shkak se duhet të implementoj konfigurimet nga një distancë për një gjysmë dymbëdhjetë pajisjesh, disa prej të cilave ndodhen mijëra kilometra larg, përdorimi i këtij instrumenti ishte paksa shqetësues.

Këtu duhet të pranoj që pasiguria ime më shumë rrjedh nga njohuria e pamjaftueshme me ansible, sesa nga dobësitë e tij. Dhe ky është, për të thënë të drejtën, një moment i rëndësishëm. ansible — është një fushë krejtësisht e veçantë, me gjuhën e saj të veçantë të domainit (Domain Specific Language), e cila duhet të mbahet në një nivel të sigurt. Po ashtu, fakti që ansible zhvillohet mjaft shpejt, pa u shqetësuar shumë për përputhshmërinë e masave, nuk i shton siguri.

Prandaj, jo shumë kohë më parë, u realizua një version i dytë i bicikletës. Këtë herë në python, më saktësisht në një framework të shkruar në python dhe për python të quajtur Nornir

Pra — Nornir është një mikroframework, e shkruar në python dhe për python dhe e destinuar për automatizimin. Ashtu siç ndodh me ansible, për zgjidhjen e detyrave këtu kërkohet përgatitje e saktë e të dhënave, pra inventarizimi i serverëve dhe parametrave të tyre, ndërsa skenarët nuk shkruhen në një DSL të veçantë, por të gjithë në atë jo shumë të vjetër, por mjaft të mirin [p|pë]jtone.

Le të shqyrtojmë se çfarë është ajo në shembullin e ardhshëm të gjallë.

Kam kam quaj një rrjet filiale me disa dhjetëra zyra në të gjithë vendin. Në çdo zyrë ka një router WAN, i cili terminon disa kanale komunikimi nga operatorë të ndryshëm. Protokolli i rrugëzimit është BGP. Routerat WAN janë të dy llojeve: Cisco ISG ose Juniper SRX.

Tani detyra është: të konfigurojmë në të gjitha routerat WAN të rrjetit filiale një nënrrjetë të dedikuar për Monitorimin Video në një port të veçantë — të shpallim këtë nënrrjetë në BGP — të konfigurojmë kufizimin e shpejtësisë për portin e dedikuar.

Së pari, na nevojitet të përgatisim një çift templates, në bazë të cilave do të gjenerohen konfigurimet veçmas për Cisco dhe Juniper. Po ashtu, duhen përgatitur të dhënat për çdo pikë dhe parametrat e lidhjes, pra të mblidhen ato inventar.

Template-i i përfunduar për 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

Templates për 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

Template, natyrisht, nuk krijohen nga asgjëja. Këto në thelb janë diferencat midis konfigurimeve operative para dhe pas zgjidhjes së detyrës në dy ruter të ndryshëm.

Nga template-tona shohim se për të zgjidhur detyrën na duhen vetëm dy parametra për Juniper dhe 3 parametra për Cisco. Këto janë:

  • ifname
  • ipsuffix
  • asn

Tani na nevojitet të caktojmë këta parametra për çdo pajisje, dmth të bëjmë atë që quhet inventory.

Për inventory do të ndjekim me rigorozitet dokumentacionin Duke inicializuar Nornir

dmth do të krijojmë një skelet të ngjashëm dosjesh:

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

Skedari config.yaml është skedari standard i konfigurimit për 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"

Parametrat kryesorë do të tregohen në skedarin hosts.yaml, gruporë (në rastin tim, ky është përdoruesi/fjalëkalimi) në groups.yaml, ndërsa në defaults.yaml nuk do të tregojmë asgjë, por aty duhet të shkruhen tre të tjera -- që tregojnë se kjo është yaml një skedar, edhe pse bosh.

Kështu duket 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

Dhe kështu duket groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Kjo është ajo që rezultoi inventory për detyrën tonë. Në fillim, parametrat nga skedarët e inventarit mapohen në modelin objektor InventoryElement.

Nën spoiler është skema e modelit InventoryElement

print(json.dumps(InventoryElement.schema(), indent=4))
{
    "title": "Elementi i Inventarit",
    "type": "object",
    "properties": {
        "hostname": {
            "title": "Hostname",
            "type": "string"
        },
        "port": {
            "title": "Porti",
            "type": "integer"
        },
        "username": {
            "title": "Emri i përdoruesit",
            "type": "string"
        },
        "password": {
            "title": "Fjalëkalimi",
            "type": "string"
        },
        "platform": {
            "title": "Platforma",
            "type": "string"
        },
        "groups": {
            "title": "Grupet",
            "default": [],
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "data": {
            "title": "Të dhënat",
            "default": {},
            "type": "object"
        },
        "connection_options": {
            "title": "Opsionet e lidhjes",
            "default": {},
            "type": "object",
            "additionalProperties": {
                "$ref": "#/definitions/ConnectionOptions"
            }
        }
    },
    "definitions": {
        "ConnectionOptions": {
            "title": "Opsionet e Lidhjes",
            "type": "object",
            "properties": {
                "hostname": {
                    "title": "Hostname",
                    "type": "string"
                },
                "port": {
                    "title": "Porti",
                    "type": "integer"
                },
                "username": {
                    "title": "Emri i përdoruesit",
                    "type": "string"
                },
                "password": {
                    "title": "Fjalëkalimi",
                    "type": "string"
                },
                "platform": {
                    "title": "Platforma",
                    "type": "string"
                },
                "extras": {
                    "title": "Shtesat",
                    "type": "object"
                }
            }
        }
    }
}

Ky kyç mund të duket pak i ngatërruar, sidomos në fillim. Një ndihmë e madhe për të kuptuar është moda interaktive në ipython.

 $ ipython3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
Shkruani 'copyright', 'credits' ose 'license' për më shumë informacion
IPython 7.1.1 -- Një Python Interaktiv e avancuar. Shkruani '?' për ndihmë.

Në [1]: from nornir import InitNornir                                                                           

Në [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)                                                

Në [3]: nr.inventory.hosts                                                                                      
Dalja [3]: 
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}

Në [4]: nr.inventory.hosts['srx-test'].data                                                                                    
Dalja [4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}

Në [5]: nr.inventory.hosts['srx-test']['task_data']                                                     
Dalja [5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}

Në [6]: nr.inventory.hosts['srx-test'].platform                                                                                
Dalja [6]: 'junos'

Dhe përfundimisht kalojmë te skripti. Në këtë rast nuk kam shumë për të qenë krenar. Thjesht kam marrë një shembull të gatshëm nga tutori dhe e kam përdorur atë gati pa ndryshime. Ja si duket skripti i gatshëm për punë:

nga nornir importo InitNornir
nga nornir.plugins.tasks import networking, text
nga nornir.plugins.functions.text import print_title, print_result

def config_and_deploy(task):
    # Transformoni të dhënat e inventarit në konfigurim përmes një skedari shabllon
    r = task.run(task=text.template_file,
                 name="Konfigurimi Bazë",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Ruani konfigurimin e kompilar në një variabël të hostit
    task.host["config"] = r.result

    # Ruani konfigurimin e kompilar në një skedar
    with open(f"configs/{task.host.hostname}", "w") si f:
        f.write(r.result)

    # Përdorni këtë konfigurim në pajisje duke përdorur NAPALM
    task.run(task=networking.napalm_configure,
             name="Ngarkimi i Konfigurimit në pajisje",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # vendosni dry_run=False, lërni dorën dhe ekzekutoni përsëri

# ekzekutoni detyrat
rezultati = nr.run(task=config_and_deploy)
print_result(rezultati)

Vini re parametrin dry_run=True në rreshtin e inicializimit të objektit nr.
Këtu gjithashtu si në ansible implementohet një provim testimi ku ndodhi lidhja me routerin, përgatitet një konfigurim i ri i ndryshuar, i cili më pas validon nga pajisja (por kjo nuk është e sigurt; varet nga mbështetje nga pajisja dhe implementimi i shoferit në NAPALM), por nuk ndodh aplikimi i drejtpërdrejtë i konfigurimit të ri. Për përdorim të luftës duhet të hiqet parametri dry_run ose ndryshoni vlerën e tij në False.

Gjatë ekzekutimit të skenarit Nornir, merren informacionet e detajuara në konsolë.

Nën spojler, rezultati i testit të luftës në dy rrugëzues testues:

config_and_deploy***************************************************************
* cisco-test ** ndryshuar : True *******************************************
vvvv config_and_deploy ** ndryshuar : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Konfigurimi Bazë ** ndryshuar : 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 lejo 10.10.222.0 0.0.0.255
access-list 111 lejo ip 10.10.222.0 0.0.0.255 any
---- Duke ngarkesën e Konfigurimit në pajisje ** ndryshuar : 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 lejo 10.10.222.0 0.0.0.255
+access-list 111 lejo ip 10.10.222.0 0.0.0.255 any
^^^^ KALIMI config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** ndryshuar : True *******************************************
vvvv config_and_deploy ** ndryshuar : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Konfigurimi Bazë ** ndryshuar : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Video surveillancë"
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
---- Duke ngarkesën e Konfigurimit në pajisje ** ndryshuar : True --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "Video surveillancë";
+           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;
+              }
+          }
+      }
+  }
^^^^ KALIMI config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ne fshihni fjalëkalimet në ansible_vault

Në fillim të artikullit kam bërë një komentin e shpejtë mbi ansible, por gjërat nuk janë aq të këqija. Më pëlqen shumë vault që është krijuar për të fshehur informacionin e ndjeshëm nga sytë e tjerë. Dhe ndoshta shumë kanë vënë re se të gjitha hyrjet/fjalëkalimet për të gjithë ruterët produktivë shfaqen hapur në skedarin gorups.yaml. Sigurisht që nuk është e bukur. Le të mbrojmë këto të dhëna duke përdorur vault.

Do të transferojmë parametrat nga groups.yaml në creds.yaml dhe do ta enkriptojmë me AES256 me një fjalëkalim 20-shifror:

$ 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

Kështu ishte e thjeshtë. Tani duhet ta mësojmë skriptin tonë të marrë dhe të aplikojë këto të dhëna. Nornir-скрипт доставать и применять эти данные.
Për këtë, në skriptin tonë pas linjës së inicializimit nr = InitNornir(config_file=… shtojmë kodin e mëposhtëm:

... 
nr = InitNornir(config_file="config.yaml", dry_run=True) # vendosni dry_run=False, kapni besimin dhe provohet përsëri

# pasuroni Inventarin me të dhënat e encryptuara të kashtës
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))

# ekzekutoni detyrat
...

Sigurisht vault.passwd nuk duhet të jetë afër creds.yaml si në shembullin tim. Por për të provuar, është në rregull.

Për momentin, kjo është e gjitha. Po vijnë edhe disa artikuj mbi Cisco + Zabbix, por kjo është paksa jashtë automatizimit. Dhe në të ardhmen e afërt planifikoj të shkruaj për RESTCONF në Cisco.

Burimi: habr.com

Bli një hosting të besueshëm për faqet me mbrojtje DDoS, VPS VDS serverë 🔥 Bli një hosting të besueshëm për faqet me mbrojtje DDoS, VPS VDS serverë | ProHoster