Nornir を使用したネットワーク デバイス構成要素の自動生成と入力

Nornir を使用したネットワーク デバイス構成要素の自動生成と入力

おい、ハブル!

最近ここに記事が出てきました MikrotikとLinux。 ルーチン化と自動化 同様の問題が化石手段を使用して解決されました。 このタスクは完全に典型的なものですが、ハブレではそれに似たものは何もありません。 私はあえて自分の自転車を尊敬される IT コミュニティに提供します。

このような用途に使用されるバイクはこれが初めてではありません。 最初のオプションは数年前に実装されました。 アンシブル バージョン 1.x.x。 自転車はほとんど使用されなかったため、常に錆びていました。 タスク自体はバージョンが更新されるほど頻繁には発生しないという意味で アンシブル。 そして、運転する必要があるたびに、チェーンが外れたり、ホイールが外れたりします。 ただし、最初の部分である構成の生成は、幸いなことに常に非常に明確に機能します。 ジンジャ2 エンジンは長い間確立されています。 しかし、XNUMX 番目の部分である構成のロールアウトでは、通常、驚きがもたらされました。 また、構成を XNUMX 台のデバイスにリモートで展開する必要があり、その一部は数千キロメートル離れた場所にあるため、このツールを使用するのは少し退屈でした。

ここで、私が確信を持てないのは、おそらく私がこの分野に精通していないことが原因であることを認めなければなりません。 アンシブル欠点よりも。 そして、ちなみに、これは重要なポイントです。 アンシブル は、独自の DSL (ドメイン固有言語) を備えた完全に独立した独自の知識領域であり、自信を持ったレベルに維持する必要があります。 さて、その瞬間、 アンシブル 非常に急速に開発されており、下位互換性を特別に考慮しない限り、信頼性は高まりません。

したがって、少し前に自転車の XNUMX 番目のバージョンが実装されました。 今度は パイソン、またはむしろ、で書かれたフレームワーク上で パイソン とのために パイソン と呼ばれる ノルニル

それで - ノルニル で書かれたマイクロフレームワークです パイソン とのために パイソン 自動化向けに設計されています。 の場合と同じ アンシブル、ここでの問題を解決するには、有能なデータ準備が必要です。 ホストとそのパラメータのインベントリを作成しますが、スクリプトは別の DSL ではなく、同じそれほど古くはないが非常に優れた p[i|i]ton で記述されます。

次の実際の例を使用して、それが何であるかを見てみましょう。

私は全国に数十のオフィスを持つ支店ネットワークを持っています。 各オフィスには、さまざまな通信事業者からの複数の通信チャネルを終端する WAN ルーターがあります。 ルーティングプロトコルはBGPです。 WAN ルーターには、Cisco ISG または Juniper SRX の XNUMX つのタイプがあります。

ここでの作業は、ブランチ ネットワークのすべての WAN ルーター上の別のポートにビデオ監視用の専用サブネットを構成する必要があります。このサブネットを BGP でアドバタイズし、専用ポートの速度制限を構成します。

まず、Cisco と Juniper 用に個別に生成される設定に基づいて、いくつかのテンプレートを準備する必要があります。 また、各ポイントのデータと接続パラメータを準備する必要があります。 同じ在庫を集める

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

ジュニパーのテンプレート:

$ 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

もちろん、テンプレートは何もないところから生まれるわけではありません。 これらは基本的に、異なるモデルの XNUMX つの特定のルータ上でタスクを解決したときの動作設定とその後の動作設定の差分です。

テンプレートから、問題を解決するには、Juniper の場合は 3 つのパラメーター、Cisco の場合は XNUMX つのパラメーターのみが必要であることがわかります。 どうぞ:

  • ifname
  • ipsuffix
  • アスン

ここで、各デバイスに対してこれらのパラメータを設定する必要があります。 同じことをする インベントリー.

のために インベントリー 私たちは文書に厳密に従います ノルニルの初期化

つまり、同じファイル スケルトンを作成しましょう。

.
├── 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 何も指定しませんが、そこに XNUMX つのマイナスを入力する必要があります。 ヤムル ただしファイルは空です。

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=True インラインオブジェクトの初期化 nr.
ここでも同様です アンシブル テスト実行が実装されており、ルーターへの接続が確立され、変更された新しい構成が準備され、デバイスによって検証されます (ただし、これは確実ではありません。デバイスのサポートと NAPALM のドライバー実装によって異なります)。ですが、新しい構成は直接適用されません。 戦闘で使用する場合は、パラメータを削除する必要があります ドライラン またはその値を次のように変更します ×.

スクリプトが実行されると、Nornir は詳細なログをコンソールに出力します。

スポイラーの下には、XNUMX 台のテスト ルーターで実行された戦闘の出力が表示されます。

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 でのパスワードの非表示

記事の冒頭で少し言い過ぎました アンシブル, しかし、それはそれほど悪いことではありません。 私は本当に彼らが好きです 金庫 機密情報を見えないように隠すように設計されています。 そして、おそらく多くの人が、すべての戦闘ルーターのすべてのログイン/パスワードがファイル内に開かれた形式で保存されていることにお気づきでしょう。 gorups.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 に関する記事はあと XNUMX つありますが、これは自動化に関するものではありません。 そして近い将来、Cisco の RESTCONF について書く予定です。

出所: habr.com

コメントを追加します