はじめに
前回・前々回に続き、今回は、通信確認表で定義した想定結果と、実際の結果が同じか、Ansibleでチェックする方法をご紹介したいと思います。
前々回:Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ①事前事後の無影響確認
前回:Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ②事前事後の差分確認
おそらくテストパターンの中で自動化が一番面倒で、実現方法も色々とあると思います。
今回はAnsible2.8で登場予定のread_csv
モジュールを使い、CSV形式の通信確認表をリスト形式に変換し、同じくリスト形式にした実際の結果と比較してみます。
通信確認表
ホスト名csr1、csr2の機器を新規導入する想定で、ルーティングテーブルの確認表を作成しました。
HOST
は確認対象のホスト名、ADD_DEL
はルート追加・削除、PROTOCOL
はEIGRP(D)や直接接続(C)、ローカル(L)、NETWORK
/MASK
は宛先ネットワーク、NEXTHOP_IP
はネクストホップIPアドレス、RESULT
は結果を記載する欄になります。
今回は簡単のために、csr1のみチェックしてみます。
Inventory
AnsibleをインストールしたLinux上で、csr1をhosts登録しています。
[cisco]
csr1 ansible_user=csr1 ansible_password=cisco ansible_become_pass=csr1
[cisco:vars]
ansible_network_os=ios
ansible_become=yes
ansible_become_method=enable
Playbook
大まかな流れとしては、
<想定結果の定義>
(1) CSV形式の通信確認表をリスト形式で出力
(2) 宛先、マスク、ネクストホップアドレス、プロトコルだけを抽出
(3) 昇順でソート
(4) 確認のため、(1)と(3)の結果をdebugで出力
<実際の結果を取得>
(5) show ip route
の取得
(6) textFSMでパース
(7) 宛先、マスク、ネクストホップアドレス、プロトコルだけを抽出
(8) 昇順でソート
(9) 確認のため、(6)と(8)の結果をdebugで出力
<通信確認>
(10) (3)と(8)の値をassertモジュールで比較
といった感じです。
(1)で出力されたリストには、csr1とcsr2両方の確認項目が含まれているため、(2)でloop(繰り返し)とwhen(条件分岐)を組み合わせ、対象となるcsr1の項目のみを抽出しています。
(3)~(10)は前回、前々回のものを流用しています。
---
- hosts: cisco
gather_facts: no
connection: network_cli
tasks:
- name: read routing data from CSV file and return a list #(1)
read_csv:
path: route_check_sheet.csv
register: route_intended
- name: extract necessary data from routing table (intended) #(2)
set_fact:
extracted_route_intended: "{{ extracted_route_intended + [[item.NETWORK, item.MASK, item.NEXTHOP_IP, item.PROTOCOL]] }}"
when: item.HOST == "{{ inventory_hostname }}"
loop: "{{ route_intended.list | flatten(levels=1) }}"
- name: sort extracted route (intended) #(3)
set_fact:
sorted_route_intended: "{{ extracted_route_intended | sort }}"
- name: debug (intended) #(4)
debug:
msg:
- "{{ route_intended }}"
- "{{ sorted_route_intended }}"
- name: run show command on remote devices (actual) #(5)
ios_command:
commands:
- show ip route
register: result_actual
- name: get parsed routing table using textfsm (actual) #(6)
set_fact:
route_actual: "{{ result_actual.stdout[0] | parse_cli_textfsm('./ntc-templates-master/templates/cisco_ios_show_ip_route.template') }}"
- name: extract necessary data from routing table (actual) #(7)
set_fact:
extracted_route_actual: "{{ extracted_route_actual + [[item.NETWORK, item.MASK, item.NEXTHOP_IP, item.PROTOCOL]] }}"
loop: "{{ route_actual | flatten(levels=1) }}"
- name: sort extracted route (actual) #(8)
set_fact:
sorted_route_actual: "{{ extracted_route_actual | sort }}"
- name: debug (actual) #(9)
debug:
msg:
- "{{ route_actual }}"
- "{{ sorted_route_actual }}"
- name: assert that intended and actual routing tables are the same #(9)
assert:
that:
- "sorted_route_intended == sorted_route_actual"
vars:
extracted_route_intended: []
extracted_route_actual: []
出力結果
(2)で、HOSTがcsr1でないものは、処理がskippingされていることが分かります。
また最後の(10)で、問題なく"msg": "All assertions passed"となっています。
[centos@localhost ansible]$ ansible-playbook -i inventory1 playbook3.yml
PLAY [cisco] *******************************************************************
TASK [read routing data from CSV file and return a list] ***********************
ok: [csr1]
TASK [extract necessary data from routing table (intended)] ********************
[WARNING]: when statements should not include jinja2 templating delimiters
such as {{ }} or {% %}. Found: item.HOST == "{{ inventory_hostname }}"
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.1.0.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.201', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.1.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.201', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.1.2.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.201', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.1.3.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.201', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.1.4.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.201', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.2.0.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.2.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.2.2.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.2.3.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'D', u'NETWORK': u'10.2.4.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'C', u'NETWORK': u'192.168.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
ok: [csr1] => (item={u'HOST': u'csr1', u'PROTOCOL': u'L', u'NETWORK': u'192.168.1.200', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'10.1.0.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'10.1.0.1', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'10.1.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'10.1.1.1', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'10.1.2.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'10.1.2.1', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'10.1.3.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'10.1.3.1', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'10.1.4.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'10.1.4.1', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'D', u'NETWORK': u'10.2.0.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'D', u'NETWORK': u'10.2.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'D', u'NETWORK': u'10.2.2.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'D', u'NETWORK': u'10.2.3.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'D', u'NETWORK': u'10.2.4.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'192.168.1.202', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'C', u'NETWORK': u'192.168.1.0', u'ADD_DEL': u'+', u'MASK': u'24', u'NEXTHOP_IP': u'', u'RESULT': u''})
skipping: [csr1] => (item={u'HOST': u'csr2', u'PROTOCOL': u'L', u'NETWORK': u'192.168.1.201', u'ADD_DEL': u'+', u'MASK': u'32', u'NEXTHOP_IP': u'', u'RESULT': u''})
TASK [sort extracted route (intended)] *****************************************
ok: [csr1]
TASK [debug (intended)] ********************************************************
ok: [csr1] => {
"msg": [
{
"changed": false,
"dict": {},
"failed": false,
"list": [
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.1.0.0",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.1.1.0",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.1.2.0",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.1.3.0",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.1.4.0",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.2.0.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.2.1.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.2.2.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.2.3.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "10.2.4.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "24",
"NETWORK": "192.168.1.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr1",
"MASK": "32",
"NETWORK": "192.168.1.200",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.1.0.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "10.1.0.1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.1.1.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "10.1.1.1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.1.2.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "10.1.2.1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.1.3.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "10.1.3.1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.1.4.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "10.1.4.1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.2.0.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.2.1.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.2.2.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.2.3.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "10.2.4.0",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "24",
"NETWORK": "192.168.1.0",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"RESULT": ""
},
{
"ADD_DEL": "+",
"HOST": "csr2",
"MASK": "32",
"NETWORK": "192.168.1.201",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"RESULT": ""
}
]
},
[
[
"10.1.0.0",
"24",
"192.168.1.201",
"D"
],
[
"10.1.1.0",
"24",
"192.168.1.201",
"D"
],
[
"10.1.2.0",
"24",
"192.168.1.201",
"D"
],
[
"10.1.3.0",
"24",
"192.168.1.201",
"D"
],
[
"10.1.4.0",
"24",
"192.168.1.201",
"D"
],
[
"10.2.0.0",
"24",
"192.168.1.202",
"D"
],
[
"10.2.1.0",
"24",
"192.168.1.202",
"D"
],
[
"10.2.2.0",
"24",
"192.168.1.202",
"D"
],
[
"10.2.3.0",
"24",
"192.168.1.202",
"D"
],
[
"10.2.4.0",
"24",
"192.168.1.202",
"D"
],
[
"192.168.1.0",
"24",
"",
"C"
],
[
"192.168.1.200",
"32",
"",
"L"
]
]
]
}
# (autualは前回、前回と同様のため省略)
TASK [assert that intended and actual routing tables are the same] *************
ok: [csr1] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY RECAP *********************************************************************
csr1 : ok=10 changed=0 unreachable=0 failed=0
[centos@localhost ansible]$
最後に
他にも、サードパーティ製のnapalm-ansibleを用いて、YAML形式で想定結果を記載する方法がありますが、read_csv
モジュールとparse_cli_textfsm
フィルターを使うことで、通信確認がかなり楽になると思いました。
ただし、現状のAnsible2.7台でread_csv
モジュールは使えませんので、最新のdevel版をGitHubからインストールするか、お薦めはしませんが、read_csv
モジュールのファイル(read_csv.py)のみをお使いのバージョンのlibraryディレクトリに格納して呼び出す必要があります。