使用Nornir自動產生和填充網路設備配置元素

使用Nornir自動產生和填充網路設備配置元素

嘿哈布爾!

最近這裡突然出現一篇文章 Mikrotik 和 Linux。常規和自動化 使用化石手段解決了類似的問題。儘管這項任務完全是典型的,但在哈布雷身上卻沒有任何相似之處。我敢於將我的自行車獻給受人尊敬的 IT 社群。

這並不是第一輛執行此類任務的自行車。第一個選項是幾年前實施的 ansible 版本 1.x.x。這輛自行車很少使用,因此不斷生鏽。從某種意義上說,任務本身並不像版本更新那樣頻繁出現 ansible。而且每次需要開車的時候,鏈條都會脫落或是車輪會脫落。然而,幸運的是,第一部分,產生配置,總是工作得非常清楚 金賈2 該引擎已經建立很長時間了。但第二部分——推出配置——通常會帶來驚喜。由於我必須將配置遠端部署到 50 台設備,其中一些設備位於數千公里之外,因此使用此工具有點無聊。

在這裡我必須承認我的不確定性很可能在於我不熟悉 ansible而非其缺點。順便說一句,這是很重要的一點。 ansible 是一個完全獨立的、有自己的知識領域,有自己的 DSL(領域特定語言),必須保持在自信的水平。嗯,那一刻 ansible 它發展得相當快,並且沒有特別考慮向後相容性,因此並沒有增加信心。

因此,不久前,第二個版本的自行車誕生了。這次在 蟒蛇,或者更確切地說,在一個編寫的框架上 蟒蛇蟒蛇諾尼爾

所以 - 諾尼爾 是一個微框架寫的 蟒蛇蟒蛇 並專為自動化而設計。與情況相同 ansible,要解決這裡的問題,需要做好資料準備,即主機及其參數的清單,但腳本不是用單獨的 DSL 編寫的,而是用同樣不是很舊但非常好的 p[i|i]ton 編寫的。

讓我們使用下面的實例來看看它是什麼。

我在全國各地擁有數十個辦事處的分公司網路。每個辦公室都有一個 WAN 路由器,用於終止來自不同營運商的多個通訊通道。路由協定是BGP。 WAN 路由器有兩種類型:Cisco ISG 或 Juniper SRX。

現在的任務是:您需要在分支網路的所有 WAN 路由器上的單獨連接埠上設定視訊監控專用子網路 - 在 BGP 中通告此子網路 - 設定專用連接埠的速度限制。

首先,我們需要準備幾個模板,在此基礎上分別為 Cisco 和 Juniper 產生設定。還需要為每個點和連接參數準備數據,即收集相同的庫存

思科現成模板:

$ 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

瞻博網路範本:

$ 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

當然,模板並不是憑空產生的。這些本質上是在不同型號的兩個特定路由器上解決任務後的工作配置之間的差異。

從我們的模板中我們看到,要解決這個問題,我們只需要 Juniper 的兩個參數和 Cisco 的 3 個參數。他們來了:

  • 名字
  • ip後綴
  • 阿森

現在我們需要為每個設備設定這些參數,即做同樣的事 庫存.

庫存 我們將嚴格遵循文檔 初始化諾尼爾

也就是說,讓我們建立相同的文件骨架:

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

config.yaml 檔案是標準的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"

我們會在文件中註明主要參數 主機.yaml,群組(在我的例子中,這些是登入名稱/密碼) 組.yaml,並在 預設值.yaml 我們不會指出任何內容,但您需要在那裡輸入三個減號 - 表明它是 雅姆 但該文件是空的。

這是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

這是 groups.yaml:

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

juniper:
    platform: junos
    username: admin2
    password: juniper2

這就是發生的事情 庫存 為了我們的任務。在初始化期間,庫存檔案中的參數被映射到物件模型 庫存元素.

劇透下方是 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"
                }
            }
        }
    }
}

這個模型可能看起來有點令人困惑,尤其是一開始。為了弄清楚這一點,互動模式 蟒蛇.

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

最後,讓我們繼續討論腳本本身。我在這裡沒有什麼值得特別自豪的。我只是拿了一個現成的例子 教學 並幾乎沒有改變地使用它。完成的工作腳本如下所示:

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)

注意參數 dry_run=真 行內物件初始化 nr.
這裡與中相同 ansible 已實現測試運行,其中與路由器建立連接,準備新的修改配置,然後由設備驗證(但這不確定;這取決於設備支援和 NAPALM 中的驅動程式實現) ,但新配置並未直接應用。對於戰鬥使用,必須刪除該參數 試運行 或將其值更改為 .

當腳本執行時,Nornir 會向控制台輸出詳細日誌。

劇透下方是在兩個測試路由器上運行的戰鬥輸出:

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

在ansible_vault中隱藏密碼

文章開頭我有點過分了 ansible,但這並沒有那麼糟。我真的很喜歡他們 拱頂 就像,它的目的是將敏感資訊隱藏在視線之外。也許很多人已經注意到,我們在一個文件中以開放形式保存了所有戰鬥路由器的所有登入/密碼 組.yaml。當然,這並不漂亮。讓我們保護這些數據 拱頂.

讓我們將參數從 groups.yaml 傳輸到 creds.yaml,並使用 256 位元密碼使用 AES20 進行加密:

$ 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

就是這麼簡單。仍然需要教導我們 諾尼爾- 用於檢索和應用此資料的腳本。
為此,請在我們的腳本中的初始化行之後 nr = InitNornir(config_file=... 新增以下程式碼:

...
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 不應像我的範例中那樣位於 creds.yaml 旁邊。不過用來玩還是可以的。

目前為止就這樣了。還有幾篇關於 Cisco + Zabbix 的文章即將發布,但這與自動化無關。在不久的將來我計劃寫一篇關於 Cisco 中的 RESTCONF 的文章。

來源: www.habr.com

添加評論