1. Nornirとは
自動化フレームワークの1つで、直接Pythonコードを書くことで機器への接続、コマンド実行、設定変更、テスト等を自動化できます。
2021/1/2時点の最新版は3.0.0です。2点台ではプラグインを含めて一つのパッケージになっていましたが、3点台からは必要なものを個別にインストールする形になりました。
プラグインの例として、Inventory処理やデータ加工・出力処理等を行うnornir_utils
、機器への接続や各種操作を行うnornir_netmiko
、nornir_napalm
、nornir_scrapli
、テンプレートからConfig生成等を行うためのnornir_jinja2
、AnsibleのInventoryファイルをNornirで使うためのnornir_ansible
等があります。
今回は、nornir_netmiko
プラグインでCisco機器に接続し、showコマンド実行と出力結果のパース、簡単なテストを行ってみました。
2. セットアップ
2-1. NW構成
Cisco CML-P 2.1の仮想環境上にIOS機器iosvl2-0
とNX-OS機器nxos-0
を構築し、2台の間でHSRPを組んでいます(ちょっと特殊ですがw)。
また上記2台と外部環境のCentOSを同一セグメント内に接続し、次項以降のソフトウェアをインストールしています。
2-2. Python
Nornirを使うには3.6.2以上が必要です。今回は仮想環境(venv)上に3.6.8をインストールしました。
2-3. Nornir + Plugins
以下最低限のものだけインストールしました。
pip install nornir
pip install nornir_utils
pip install nornir_scrapli
pip install nornir_netmiko
2-4. パーサー
TextFSM(NTC-Templates)とpyATSの2種類を使ってみます。前者は2-3.で一緒にインストールされたため、pyATSのみ追加インストールしました。
pip install pyats
だとpyATSのコア部分しかインストールされないため、以下の通りパーサー等のpyATSライブラリを含めるオプション指定が必要です。
pip install pyats[library]
(参考) パース可能なshowコマンド一覧
NTC-Templates
pyATS
3. Inventoryの作成
まず、機器へのログイン情報や実行コマンド等を指定するファイル群を作成します。Ansibleで言うところのInventoryファイルや変数定義ファイルに相当するかと思います。
プラグインとしてSimpleInventory
を用いる場合、大きく3種類のファイルがあります。
3-1. hostsファイル
各機器固有の変数を定義するファイルです。
ここではログインIPアドレス/ポート番号、プラットフォーム、所属グループ、実行コマンド、HSRPの想定結果を指定しています。
プラットフォームは、NetmikoやNAPALMからNW機器に接続する際、OSやプロトコル毎に用意されているドライバーを指定するための変数です。
具体的には、iosvl2-0
で指定しているプラットフォームcisco_ios_telnet
は、NetmikoからIOSへTelnet接続するためのドライバー名のことです。
一方nxos-0
で指定しているcisco_nxos
は、NetmikoからNX-OSへSSH接続するためのドライバー名です。
AnsibleやNAPALMでは、同様の用途でcisco_ios
やcisco_nxos
の代わりにios
やnxos
という名前が使われますが、nornir_netmiko
プラグイン内で2つの対応関係がマッピングされており、どちらを使っても問題なくログイン可能です。
所属グループは、次項のgroupsファイルとの紐付けに使用されます。
実行コマンドは、HSRPの確認コマンドが機種によって異なるためここで定義しています。
HSRPの想定結果は、最後のテストで使用します。
---
iosvl2-0:
hostname: 192.168.100.55
port: 23
platform: cisco_ios_telnet
groups:
- ios
data:
command: show standby all
hsrp:
priority: 120
state: active
nxos-0:
hostname: 192.168.100.56
port: 22
platform: cisco_nxos
groups:
- nxos
data:
command: show hsrp all
hsrp:
priority: 110
state: standby
3-2. groupsファイル
グループ共通の変数を定義するファイルです。
ここではログインユーザ名/パスワード、enableパスワードを指定しています。
enableパスワードの書き方が少し特殊で、以下の通りNetmiko用のextras
キー内で定義します。
---
ios:
username: cisco
password: cisco
connection_options:
netmiko:
extras:
secret: cisco
nxos:
username: admin
password: cisco
3-3. defaultsファイル
デフォルトで使用する変数を定義するファイルです。
ここでは全機器で共通して使う、NTPサーバ設定コマンドを指定しています。
機器によって例外がある場合は、defaultsファイルに定義しつつ、hosts、groupsファイルで値を上書きする使い方も出来ます。
---
data:
ntp_server:
- 1.1.1.1
- 2.2.2.2
4. Configファイル
Inventoryの保管パスや同時接続数等の設定情報を指定するファイルです。
---
inventory:
plugin: SimpleInventory
options:
host_file: "inventory/hosts.yaml"
group_file: "inventory/groups.yaml"
defaults_file: "inventory/defaults.yaml"
runner:
plugin: threaded
options:
num_workers: 30
5. Tasks (Pythonコード) と実行結果
5-1. 例1: showコマンド実行
2台の機器へコマンドshow version
を実行する例です。
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_command
# 初期化
nr = InitNornir(config_file="config.yaml")
# タスクを実行し、result変数に格納
result = nr.run(
# 実行内容に任意で名前を付けられる。指定しない場合はタスク名が使用される
name="run show command",
# nornir_netmikoプラグイン内のコマンド実行用関数を指定
task=netmiko_send_command,
# enableモードに昇格するか。デフォルトはFalseのため、showコマンドによっては明示的にTrue指定が必要
enable=True,
# 実行コマンド(str型)
command_string="show version"
)
# 実行結果を表示
print_result(result)
実行結果
途中省略していますが、2台とも問題なくshowコマンド結果が表示されました。
$ python nornir11.py
run show command****************************************************************
* iosvl2-0 ** changed : False **************************************************
vvvv run show command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Cisco IOS Software, vios_l2 Software (vios_l2-ADVENTERPRISEK9-M), Version 15.2(CML_NIGHTLY_20190423)FLO_DSGS7, EARLY DEPLOYMENT DEVELOPMENT BUILD, synced to V152_6_0_81_E
(省略)
^^^^ END run show command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : False ****************************************************
vvvv run show command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Cisco Nexus Operating System (NX-OS) Software
(省略)
^^^^ END run show command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5-2. 例2: showコマンド実行 + TextFSM + 実行結果限定
2台の機器へコマンドshow version
を実行し、TextFSMでパースした結果を出力する例です。
実行結果はnxos-0
だけに絞ってみました。
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_command
nr = InitNornir(config_file="config.yaml")
result = nr.run(
name="run show command",
task=netmiko_send_command,
enable=True,
command_string="show version",
# TextFSM使用オプションをTrueに指定
use_textfsm=True
)
# nxos-0の実行結果のみ表示
print_result(result["nxos-0"][0])
実行結果
nxos-0
のパース結果が表示されました。
$ python nornir12.py
---- nxos-0: run show command ** changed : False ------------------------------- INFO
[ { 'boot_image': 'bootflash:///titanium-d1-kickstart.7.3.0.D1.1.bin',
'hostname': 'nxos-0',
'last_reboot_reason': '',
'os': '7.3(0)D1(1)',
'platform': 'OSv',
'uptime': '0 day(s), 4 hour(s), 18 minute(s), 23 second(s)'}]
ちなみに結果は、リストオブジェクトに辞書がネストされたデータに見えますが、Resultという独自のオブジェクトです。リストや辞書のようにイテラブルではないため、result["nxos-0"][0][0]["os"]
のように指定しても「TypeError: 'Result' object does not support indexing」で失敗しました。
5-3. 例3: hostsファイルのコマンド実行 + pyATS + HSRPステータス判定
hostsファイルで定義したHSRPのshowコマンドを実行し、pyATSでパースした結果を出力します。
さらにパース結果からHSRPのプライオリティ値とステータス(Active/Standy)が想定通りか判定してみます。
ちなみにHSRPコマンドのパース結果は階層が深くなっており、通常の辞書データであればdata.result["階層1"]["階層2"]["階層3"]["階層4"]["階層5"]...['priority']
のように書く必要がありますが、pyATSのDq(Data Query)を使うことでDq(data.result).get_values('priority')
とシンプルな記述になっています
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_command
from genie.utils import Dq
def verify_hsrp_status(task: Task) -> Result:
data = task.run(
name="run show command from hosts file and parse the output",
task=netmiko_send_command,
enable=True,
# hostsファイルからshowコマンドを取得
command_string=nr.inventory.hosts[task.host.name]["command"],
# pyATS使用オプションをTrueに指定
use_genie=True
)
# hostsファイルで定義したHSRPプライオリティの想定値と実際の値が同じかどうか判定
assert task.host["hsrp"]["priority"] == Dq(data.result).get_values('priority')[0]
# hostsファイルで定義したHSRPステータスの想定値と実際の値が同じかどうか判定
assert task.host["hsrp"]["state"] == Dq(data.result).get_values('hsrp_router_state')[0]
nr = InitNornir(config_file="config.yaml")
# 上記のカスタムタスクを実行し、結果をresultに格納
result = nr.run(
task=verify_hsrp_status
)
# 実行結果を表示
print_result(result)
# 失敗したホストを表示
print(result.failed_hosts)
実行結果
いずれもエラーなしで問題なく完了しました。
$ python nornir14.py
verify_hsrp_status**************************************************************
* iosvl2-0 ** changed : False **************************************************
vvvv verify_hsrp_status ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- run show command from hosts file and parse the output ** changed : False -- INFO
{ 'GigabitEthernet0/0': { 'address_family': { 'ipv4': { 'version': { 1: { 'groups': { 1: { 'active_router': 'local',
'configured_priority': 120,
'group_number': 1,
'hsrp_router_state': 'active',
'local_virtual_mac_address': '0000.0c07.ac01',
'local_virtual_mac_address_conf': 'v1 '
'default',
'preempt': True,
'primary_ipv4_address': { 'address': '192.168.100.54'},
'priority': 120,
'session_name': 'hsrp-Gi0/0-1',
'standby_expires_in': 11.344,
'standby_ip_address': '192.168.100.56',
'standby_priority': 110,
'standby_router': '192.168.100.56',
'timers': { 'hello_msec_flag': False,
'hello_sec': 3,
'hold_msec_flag': False,
'hold_sec': 10,
'next_hello_sent': 2.368},
'virtual_mac_address': '0000.0c07.ac01',
'virtual_mac_address_mac_in_use': True}}}}}},
'interface': 'GigabitEthernet0/0',
'redirects_disable': False,
'use_bia': False}}
^^^^ END verify_hsrp_status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : False ****************************************************
vvvv verify_hsrp_status ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- run show command from hosts file and parse the output ** changed : False -- INFO
{ 'Ethernet2/1': { 'address_family': { 'ipv4': { 'version': { 1: { 'groups': { 1: { 'active_expire': 1.521,
'active_ip_address': '192.168.100.55',
'active_priority': 120,
'active_router': '192.168.100.55',
'authentication': 'cisco',
'configured_priority': 110,
'group_number': 1,
'hsrp_router_state': 'standby',
'last_state_change': '07:09:13',
'lower_fwd_threshold': 1,
'num_state_changes': 4,
'primary_ipv4_address': { 'address': '192.168.100.54',
'virtual_ip_learn': False},
'priority': 110,
'session_name': 'hsrp-Eth2/1-1',
'standby_priority': 110,
'standby_router': 'local',
'timers': { 'hello_msec_flag': False,
'hello_sec': 3,
'hold_msec_flag': False,
'hold_sec': 10},
'upper_fwd_threshold': 110,
'virtual_mac_address': '0000.0c07.ac01',
'virtual_mac_address_status': 'default'}}}}}},
'interface': 'Ethernet2/1',
'use_bia': False}}
^^^^ END verify_hsrp_status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{}
iosvl2-0
のHSRPプライオリティ値を120から100に変更し、Active/Standby切替を発生させた時の結果も貼り付けておきます。
verify_hsrp_statusがERRORとなり、AssertionError
の例外処理が走っています。
$ python nornir14.py
verify_hsrp_status**************************************************************
* iosvl2-0 ** changed : False **************************************************
vvvv verify_hsrp_status ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
Traceback (most recent call last):
File "/home/centos/venv_nornir3/lib64/python3.6/site-packages/nornir/core/task.py", line 98, in start
r = self.task(self, **self.params)
File "nornir14.py", line 19, in verify_hsrp_status
assert task.host["hsrp"]["priority"] == Dq(data.result).get_values('priority')[0]
AssertionError
---- run show command from hosts file and parse the output ** changed : False -- INFO
{ 'GigabitEthernet0/0': { 'address_family': { 'ipv4': { 'version': { 1: { 'groups': { 1: { 'active_expires_in': 9.648,
'active_ip_address': '192.168.100.56',
'active_router': '192.168.100.56',
'active_router_priority': 110,
'default_priority': 100,
'group_number': 1,
'hsrp_router_state': 'standby',
'local_virtual_mac_address': '0000.0c07.ac01',
'local_virtual_mac_address_conf': 'v1 '
'default',
'preempt': True,
'primary_ipv4_address': { 'address': '192.168.100.54'},
'priority': 100,
'session_name': 'hsrp-Gi0/0-1',
'standby_router': 'local',
'timers': { 'hello_msec_flag': False,
'hello_sec': 3,
'hold_msec_flag': False,
'hold_sec': 10,
'next_hello_sent': 0.912},
'virtual_mac_address': '0000.0c07.ac01',
'virtual_mac_address_mac_in_use': False}}}}}},
'interface': 'GigabitEthernet0/0',
'redirects_disable': False,
'use_bia': False}}
^^^^ END verify_hsrp_status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : False ****************************************************
vvvv verify_hsrp_status ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
Traceback (most recent call last):
File "/home/centos/venv_nornir3/lib64/python3.6/site-packages/nornir/core/task.py", line 98, in start
r = self.task(self, **self.params)
File "nornir14.py", line 21, in verify_hsrp_status
assert task.host["hsrp"]["state"] == Dq(data.result).get_values('hsrp_router_state')[0]
AssertionError
---- run show command from hosts file and parse the output ** changed : False -- INFO
{ 'Ethernet2/1': { 'address_family': { 'ipv4': { 'version': { 1: { 'groups': { 1: { 'active_priority': 110,
'active_router': 'local',
'authentication': 'cisco',
'configured_priority': 110,
'group_number': 1,
'hsrp_router_state': 'active',
'last_state_change': '00:03:44',
'lower_fwd_threshold': 1,
'num_state_changes': 5,
'primary_ipv4_address': { 'address': '192.168.100.54',
'virtual_ip_learn': False},
'priority': 110,
'session_name': 'hsrp-Eth2/1-1',
'standby_expire': 0.676,
'standby_ip_address': '192.168.100.55',
'standby_priority': 100,
'standby_router': '192.168.100.55',
'timers': { 'hello_msec_flag': False,
'hello_sec': 3,
'hold_msec_flag': False,
'hold_sec': 10},
'upper_fwd_threshold': 110,
'virtual_mac_address': '0000.0c07.ac01',
'virtual_mac_address_status': 'default'}}}}}},
'interface': 'Ethernet2/1',
'use_bia': False}}
^^^^ END verify_hsrp_status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{'iosvl2-0': MultiResult: [Result: "verify_hsrp_status", Result: "run show command from hosts file and parse the output"], 'nxos-0': MultiResult: [Result: "verify_hsrp_status", Result: "run show command from hosts file and parse the output"]}
最後に
まだ触り始めたばかりですが、AnsibleのようなInventoryの仕組みを持ちつつ、他のPythonベースの自動化ツールとの連携のしやすさ、マルチベンダ対応、Pythonでデータ加工や繰り返し処理が出来る点から、個人的に重宝しそうだと感じました。ただし、Nornirのコードの書き方に加え、PythonやベースとなるNetmikoやNAPALM等の知識が必要なので、学習コストは比較的高めなのかもしれません。
参考URL
-
公式サイト
nornir - Read the Docs
Nornir Plugins
GitHub - nornir_netmiko -
ブログ記事等
An Introduction to Nornir
[nornir] Python 製自動化フレームワーク「nornir」かんたんチュートリアル(Ansibleと比較しながら)
[nornir] 指定できるプラットフォーム名(ios / junos / eos / vyos など)
Nornir 3.0の注意点とCisco IOS XE ホスト名の取得
Passing enable secret
pyATS と Python辞書 が楽しくなる Dq(Data Query) を極める!!!