Võrguseadmete konfiguratsioonielementide automaatne genereerimine ja üleslaadimine Nornir'i abil

Võrguseadmete konfiguratsioonielementide automaatne genereerimine ja üleslaadimine Nornir'i abil

Tere, Habr!

Hiljuti ilmus siin artikkel Mikrotik ja Linux. Rutiin ja automatiseerimine kus sarnast ülesannet lahendati eelajalooliste vahenditega. Ja kuigi ülesanne on täiesti tüüpiline, ei leidu selle kohta Habras midagi sarnast. Julgen pakkuda austatud IT-kogukonnale oma jalgratast.

See ei ole esimene jalgratas sarnase ülesande jaoks. Esimene variant реализован mõned aastad tagasi veel versioonil ansible 1.x.x. Jalgrattast kasutati harva ja seetõttu see pidevalt roostetama. Just selles mõttes, et ülesanne ei teki nii tihti kui uuendused ansible. Ja iga kord, kui tuleb sõita, siis kas kett kukub maha või ratas tuleb lahti. Siiski, esimene osa, konfiguratsioonide genereerimine – töötab alati väga täpselt, õnneks jinja2 mootor on juba pikka aega kindel. Aga teine osa – konfiguratsioonide rakendamine, tavaliselt esitab üllatusi. Kuna pean rakendama konfiguratsioone kaugel umbes poolele sajale seadmele, millest mõned asuvad tuhandete kilomeetrite kaugusel, siis oli selle tööriista kasutamine veidi hirmutav.

Siin tuleb tunnistada, et minu ebakindlus tuleneb pigem piisava tuttavuse puudumisest ansible, kui selle puudustest. Ja see on, muide, oluline punkt. ansible – see on täiesti eriline, omaette teadmiste valdkond oma DSL (Domain Specific Language) -ga, millel tuleb hoida kindel tase. Ja see, et ansible areng toimub üsna kiiresti, ilma eriliste tagasipöördumiste arvestamiseta, ei lisa kindlust.

Seetõttu hiljuti valmis teine variant jalgrattast. Seekord põhines see python, täpsemalt raamistikul, mis on kirjutatud python kui ka python nimetusega Nornir

Nii et – Nornir see on mikrotehnikum, kirjutatud python kui ka python ja mõeldud automatiseerimiseks. Nagu ka juhtudel ansible, ülesannete lahendamiseks on siin vajalik andmete korralik ettevalmistamine, st hostide ja nende parameetrite inventuurimine, kuid skriptid kirjutatakse mitte eraldi DSL-is, vaid kõik samas mitte väga vanas, kuid väga heades p[ü|i]thoni vormis.

Vaadakem, mis see on järgmise elava näite kaudu.

Mul on filiaalide võrk, kus on mitukümmend kontorit üle kogu riigi. Igas kontoris on WAN-marsruuter, mis terminiseerib mitu sidekanalit erinevatelt operaatoritelt. Marsruutimisprotokoll on BGP. WAN-marsruuterid on kahte tüüpi: Cisco ISG või Juniper SRX.

Nüüd on ülesanne: tuleb konfigureerida kõikides WAN-marsruutijates eraldi vahemik videovalve jaoks eraldi seadmel – kuulutada see vahemik BGP-s välja – konfigureerida kiiruspiirang eraldatud sadama jaoks.

Esmalt tuleb meil valmistada kaks šablooni, mille alusel genereeritakse konfigureerimised eraldi Cisco ja Juniper jaoks. Samuti tuleb ette valmistada andmed iga punkti ja ühendamise parameetrite kohta, st koguda see sama inventuur.

Valmis šabloon Cisco jaoks:

$ 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

Šabloon Juniper jaoks:

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

Šabloonid, muidugi, ei tulene lihtsalt õhust. Need on põhimõtteliselt diffid usaldusväärsete konfigureeringute vahel, mis olid enne ja pärast ülesande lahendamist kahe konkreetse erineva marsruutimisseadmestiku vahel.

Meie šabloonidest näeme, et ülesande lahendamiseks piisab kahest parameetrist Juniper ja kolmest parameetrist Cisco jaoks. Need on:

  • ifname
  • ipsuffix
  • asn

Nüüd peame määrama need parameetrid iga seadme jaoks, st tegema selle sama inventory.

Kuna inventory järgime selgelt dokumentatsiooni Initializing Nornir

st loome sarnase failistruktuuri:

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

config.yaml fail – on nornir'i standardne konfiguratsioonifail

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

Peamised parameetrid määrame failis hosts.yaml, rühmad (minu puhul need on logid/paroolid) sisse groups.yaml, ja seejärel määratleme selle, takistades seeläbi kasutajal selgelt selle väljaid muuta. See on üks andmete peitmise mustritest defaults.yaml ei pea midagi näitama, kuid sinna on vajalik kirjutada kolm miinusmärki — mis näitavad, et see on yaml fail on küll tühi.

Nii näeb välja 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

Nii näeb välja groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Nii on see välja kukkunud inventory meie ülesande jaoks. Initsialiseerimisel kaardistatakse parameetrid inventori failidest objektimudelile InventoryElement.

Spoileri all on InventoryElement mudeli skeem

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

See mudel võib esmapilgul tunduda pisut segane. Selle mõistmiseks aitab palju interaktiivne režiim 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 -- Täiendatud Interaktiivne 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'

Ja lõpuks liigume tõeliselt skripti juurde. Sellest mul eriti uhke olla ei ole. Lihtsalt kasutasin valmi näidet tutvustuses ja kasutasin seda peaaegu muutumatuna. Nii näeb välja valmis tööalune 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):
    # Muuda inventari andmed konfiguratsiooniks mallifaili kaudu
    r = task.run(task=text.template_file,
                 name="Base Configuration",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Salvestage koostatud konfiguratsioon seadme muutuja
    task.host["config"] = r.result

    # Salvestage koostatud konfiguratsioon faili
    with open(f"configs/{task.host.hostname}", "w") as f:
        f.write(r.result)

    # Rakenda see konfiguratsioon seadmele NAPALMi kasutades
    task.run(task=networking.napalm_configure,
             name="Seadme konfiguratsiooni laadimine",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # seadista dry_run=False, hoia pöialt ja käivita uuesti

# käivita ülesanded
result = nr.run(task=config_and_deploy)
print_result(result)

Pöörake tähelepanu parameetrile dry_run=True objekti initsialiseerimise reas nr.
Siin on samuti nagu ansible teostatakse testjooks, mille käigus ühendatakse ruuteriga, valmistatakse ette uus muudetud konfiguratsioon, mida seade tõendab (aga see ei pruugi nii olla; see sõltub seadme toest ja NAPALMi draiveri rakendamisest), kuid uue konfiguratsiooni rakendamine ei toimu. Tegelikuks rakendamiseks tuleb eemaldada parameeter dry_run või muuta selle väärtust False.

Norniri skripti käivitamisel näitab see konsoolis üksikasjalikke logsid.

Spoileri all on tootmisjooksu väljund kahe testruuteri peal:

config_and_deploy***************************************************************
* cisco-test ** muutunud : True *******************************************
vvvv config_and_deploy ** muutunud : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Baas Konfiguratsioon ** muutunud : 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 lubage 10.10.222.0 0.0.0.255
access-list 111 lubage ip 10.10.222.0 0.0.0.255 any
---- Seade konfiguratsiooni laadimine ** muutunud : 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 lubage 10.10.222.0 0.0.0.255
+access-list 111 lubage ip 10.10.222.0 0.0.0.255 any
^^^^ LÕPP config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** muutunud : True *******************************************
vvvv config_and_deploy ** muutunud : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Baas Konfiguratsioon ** muutunud : 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
---- Seade konfiguratsiooni laadimine ** muutunud : 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;
+              }
+          }
+      }
+  }
^^^^ LÕPP config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Peidame paroolid ansible_vault

Artikli alguses tegin ma veidi liiga ansible, kuid seal ei ole kõik nii hull. Mulle tõeliselt meeldib nende vault ja see on mõeldud tundlike andmete varjamiseks. Ja ilmselt on paljud märganud, et meil on kõik logi/paroolid kõigi tootmisseadmete jaoks avatud failis gorups.yaml. See on ilmselgelt kole. Kaitseme need andmed vault.

Kandke parameetrid groups.yaml failist creds.yaml faili ja krüpteerime selle AES256 20-kohalise parooliga:

$ 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  
Krüptimine õnnestus
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

Nii lihtne. Nüüd tuleb õpetada meie Nornir-skripti andmete hankimist ja rakendamist.
Selleks lisame meie skripti pärast initsialiseerimise rida nr = InitNornir(config_file=… järgmise koodi:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # sette dry_run=False, loodan parimale ja käivitan uuesti

# rikastame inventari krüpteeritud vault andmetega
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))

# tööde käivitamine
...

Muidugi ei tohiks vault.passwd olla koos creds.yaml failiga, nagu minu näites. Kuid harjutamiseks on see piisav.

Sellega läheb kõik. Peagi on veel paar artiklit Cisco + Zabbix kohta, kuid see ei ole otseselt automatiseerimine. Juba varsti plaanin kirjutada Cisco RESTCONF-ist.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster