Awtomatikong pagbuo at pagpuno ng mga elemento ng configuration ng network device gamit ang Nornir

Awtomatikong pagbuo at pagpuno ng mga elemento ng configuration ng network device gamit ang Nornir

Hoy Habr!

Kamakailan ay isang artikulo ang lumitaw dito Mikrotik at Linux. Routine at automation kung saan ang isang katulad na problema ay nalutas gamit ang fossil na paraan. At kahit na ang gawain ay ganap na tipikal, walang katulad nito sa Habré. Naglakas-loob akong ialay ang aking bisikleta sa iginagalang na komunidad ng IT.

Hindi ito ang unang bike para sa naturang gawain. Ang unang opsyon ay ipinatupad ilang taon na ang nakalipas pabalik ansible bersyon 1.x.x. Ang bisikleta ay bihirang ginagamit at samakatuwid ay patuloy na kinakalawang. Sa diwa na ang gawain mismo ay hindi lilitaw nang kasingdalas ng pag-update ng mga bersyon ansible. At sa tuwing kailangan mong magmaneho, nahuhulog ang kadena o nahuhulog ang gulong. Gayunpaman, ang unang bahagi, ang pagbuo ng mga config, ay palaging gumagana nang napakalinaw, sa kabutihang palad jinja2 Matagal nang naitatag ang makina. Ngunit ang pangalawang bahagi - ang paglulunsad ng mga config - ay kadalasang nagdadala ng mga sorpresa. At dahil kailangan kong i-roll out ang config nang malayuan sa kalahating daang device, ang ilan sa mga ito ay matatagpuan libu-libong kilometro ang layo, ang paggamit ng tool na ito ay medyo nakakainip.

Dito ko dapat aminin na ang aking kawalan ng katiyakan ay malamang na nakasalalay sa aking kawalan ng pamilyar sa ansiblekaysa sa mga pagkukulang nito. At ito, sa pamamagitan ng paraan, ay isang mahalagang punto. ansible ay isang ganap na hiwalay, ang sarili nitong lugar ng kaalaman na may sariling DSL (Domain Specific Language), na dapat mapanatili sa antas ng kumpiyansa. Well, sa sandaling iyon ansible Ito ay mabilis na umuunlad, at walang espesyal na pagsasaalang-alang sa paatras na pagkakatugma, hindi ito nagdaragdag ng kumpiyansa.

Samakatuwid, hindi pa matagal na ang nakalipas ay ipinatupad ang pangalawang bersyon ng bisikleta. This time on python, o sa halip sa isang balangkas na nakasulat sa python at para sa python tinawagan Nornir

Kaya- Nornir ay isang microframework na nakasulat sa python at para sa python at dinisenyo para sa automation. Ang parehong bilang sa kaso sa ansible, upang malutas ang mga problema dito, kinakailangan ang karampatang paghahanda ng data, i.e. imbentaryo ng mga host at kanilang mga parameter, ngunit ang mga script ay hindi nakasulat sa isang hiwalay na DSL, ngunit sa parehong hindi masyadong luma, ngunit napakahusay na p[i|i]ton.

Tingnan natin kung ano ang gamit nito sa sumusunod na live na halimbawa.

Mayroon akong network ng sangay na may ilang dosenang mga opisina sa buong bansa. Ang bawat opisina ay may WAN router na nagtatapos sa ilang mga channel ng komunikasyon mula sa iba't ibang mga operator. Ang routing protocol ay BGP. Ang mga WAN router ay may dalawang uri: Cisco ISG o Juniper SRX.

Ngayon ang gawain: kailangan mong i-configure ang isang nakalaang subnet para sa Video Surveillance sa isang hiwalay na port sa lahat ng WAN routers ng branch network - i-advertise ang subnet na ito sa BGP - i-configure ang speed limit ng nakalaang port.

Una, kailangan naming maghanda ng ilang mga template, batay sa kung aling mga pagsasaayos ang bubuo nang hiwalay para sa Cisco at Juniper. Kinakailangan din na maghanda ng data para sa bawat punto at mga parameter ng koneksyon, i.e. kolektahin ang parehong imbentaryo

Handa na template para sa 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

Template para sa 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

Ang mga template, siyempre, ay hindi lumalabas sa manipis na hangin. Ang mga ito ay mahalagang pagkakaiba sa pagitan ng mga gumaganang configuration noon at noon pagkatapos malutas ang gawain sa dalawang partikular na router ng magkaibang mga modelo.

Mula sa aming mga template nakita namin na upang malutas ang problema, kailangan lang namin ng dalawang parameter para sa Juniper at 3 parameter para sa Cisco. nandito na sila:

  • ifname
  • ipsuffix
  • asn

Ngayon ay kailangan nating itakda ang mga parameter na ito para sa bawat device, i.e. gawin ang parehong bagay imbentaryo.

Para sa imbentaryo Mahigpit naming susundin ang dokumentasyon Sinisimulan si Nornir

ibig sabihin, gumawa tayo ng parehong balangkas ng file:

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

Ang config.yaml file ay ang karaniwang nornir configuration file

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

Ipahiwatig namin ang pangunahing mga parameter sa file hosts.yaml, grupo (sa aking kaso ito ay mga pag-login/password) sa mga grupo.yamlat sa defaults.yaml Hindi namin ipahiwatig ang anumang bagay, ngunit kailangan mong magpasok ng tatlong minus doon - na nagpapahiwatig na ito ay yaml ang file ay walang laman bagaman.

Ito ang hitsura ng 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

At narito ang mga grupo.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

Ito ang nangyari imbentaryo para sa ating gawain. Sa panahon ng pagsisimula, ang mga parameter mula sa mga file ng imbentaryo ay nakamapa sa object model InventoryElement.

Sa ibaba ng spoiler ay isang diagram ng modelo ng 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"
                }
            }
        }
    }
}

Ang modelong ito ay maaaring magmukhang medyo nakalilito, lalo na sa una. Upang malaman ito, pumasok ang interactive na mode sawa.

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

At sa wakas, lumipat tayo sa script mismo. Wala akong maipagmamalaki lalo na dito. Kumuha lang ako ng isang handa na halimbawa mula sa pagtuturo at ginamit ito halos hindi nagbabago. Ganito ang hitsura ng natapos na gumaganang script:

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)

Bigyang-pansin ang parameter dry_run=Totoo sa line object initialization nr.
Dito katulad ng sa ansible isang test run ang ipinatupad kung saan ang isang koneksyon sa router ay ginawa, isang bagong binagong configuration ay inihanda, na pagkatapos ay na-validate ng device (ngunit hindi ito tiyak; ito ay depende sa suporta ng device at ang pagpapatupad ng driver sa NAPALM) , ngunit hindi direktang inilapat ang bagong configuration. Para sa paggamit ng labanan, dapat mong alisin ang parameter dry_run o baguhin ang halaga nito sa Huwad.

Kapag naisakatuparan ang script, naglalabas si Nornir ng mga detalyadong log sa console.

Sa ibaba ng spoiler ay ang output ng isang combat run sa dalawang test router:

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

Pagtatago ng mga password sa ansible_vault

Sa simula ng artikulo ay lumampas ako nang kaunti ansible, ngunit hindi lahat ng ito ay masama. gustong gusto ko sila Hanay ng mga arko tulad ng, na idinisenyo upang itago ang sensitibong impormasyon na hindi nakikita. At marahil marami ang nakapansin na mayroon kaming lahat ng mga login/password para sa lahat ng mga combat router na kumikinang sa bukas na anyo sa isang file gorups.yaml. Hindi ito maganda, siyempre. Protektahan natin ang data na ito gamit ang Hanay ng mga arko.

Ilipat natin ang mga parameter mula sa groups.yaml patungo sa creds.yaml, at i-encrypt ito gamit ang AES256 na may 20-digit na password:

$ 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

Ganun kasimple. Ito ay nananatiling magturo sa ating Nornir-script para kunin at ilapat ang data na ito.
Upang gawin ito, sa aming script pagkatapos ng linya ng pagsisimula nr = InitNornir(config_file=… idagdag ang sumusunod na code:

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

Siyempre, hindi dapat matatagpuan ang vault.passwd sa tabi ng creds.yaml tulad ng sa aking halimbawa. Pero ok lang sa paglalaro.

Yun lang muna. Mayroong ilang higit pang mga artikulo tungkol sa Cisco + Zabbix na darating, ngunit hindi ito tungkol sa automation. At sa malapit na hinaharap plano kong magsulat tungkol sa RESTCONF sa Cisco.

Pinagmulan: www.habr.com

Magdagdag ng komento