はじめに
Ansibleのparse_cli_textfsm
フィルター、set_fact
モジュール、assert
モジュールを組み合わせると、CLIコマンド出力結果を元に様々なネットワークテストができます。
以前Cisco IOSで、EIGRPのネイバーが存在するか、UPTIMEが1分より大きいか確認するPlaybookをご紹介しました。
Ansibleのparse_cli_textfsmフィルターを使ったネットワークテストの自動化
上記は想定結果を指定するパターンですが、それ以外にも、事前事後でステータスが変わらないことを確認する場面も良くあるかと思います。
(NW機器のリプレイス、OSアップグレード、その他小規模メンテナンス作業等)
今回はCisco IOSのshow ip route
コマンドで表示される宛先、ネクストホップ、プロトコルが、事前事後で変わらないことを確認するPlaybookを作成してみました。
Inventory
3台のCisco CSR1000Vを用意し、お互いにEIGRPでルーティング情報をやりとりする構成にしています。
確認結果をシンプルにするため、Inventory上は1台のみ指定しています。
[cisco]
192.168.1.200 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) show ip route
の取得
(2) textFSMでパース
(3) 宛先、マスク、ネクストホップアドレス、プロトコルだけを抽出
(4) 昇順でソート
(5) 確認のため、(2)と(4)の結果をdebugで出力
<事後作業>
(6)~(10) 上記(1)~(5)と同じことを実施
(11) (5)と(10)の値をassertモジュールで比較
といった感じです。
実際のケースでは、(5)と(6)の間でOSアップグレードなどを行うイメージです。
---
- hosts: cisco
gather_facts: no
connection: network_cli
tasks:
- name: run show command on remote devices (before) #(1)
ios_command:
commands:
- show ip route
register: result_before
- name: get parsed routing table using textfsm (before) #(2)
set_fact:
route_before: "{{ result_before.stdout[0] | parse_cli_textfsm('./ntc-templates-master/templates/cisco_ios_show_ip_route.template') }}"
- name: extract necessary data from routing table (before) #(3)
set_fact:
extracted_route_before: "{{ extracted_route_before + [[item.NETWORK, item.MASK, item.NEXTHOP_IP, item.PROTOCOL]] }}"
loop: "{{ route_before | flatten(levels=1) }}"
- name: sort extracted route (before) #(4)
set_fact:
sorted_route_before: "{{ extracted_route_before | sort }}"
- name: debug (before) #(5)
debug:
msg:
- "{{ route_before }}"
- "{{ sorted_route_before }}"
- name: run show command on remote devices (after) #(6)
ios_command:
commands:
- show ip route
register: result_after
- name: get parsed routing table using textfsm (after) #(7)
set_fact:
route_after: "{{ result_after.stdout[0] | parse_cli_textfsm('./ntc-templates-master/templates/cisco_ios_show_ip_route.template') }}"
- name: extract necessary data from routing table (after) #(8)
set_fact:
extracted_route_after: "{{ extracted_route_after + [[item.NETWORK, item.MASK, item.NEXTHOP_IP, item.PROTOCOL]] }}"
loop: "{{ route_after | flatten(levels=1) }}"
- name: sort extracted route (after) #(9)
set_fact:
sorted_route_after: "{{ extracted_route_after | sort }}"
- name: debug (after) #(10)
debug:
msg:
- "{{ route_after }}"
- "{{ sorted_route_after }}"
- name: assert that before and ater routing tables are the same #(11)
assert:
that:
- "sorted_route_before == sorted_route_after"
vars:
extracted_route_before: []
extracted_route_after: []
出力結果
長いので(6)~(10)は省略しています。
当然と言えば当然なのですが、(11)の比較で差異がないことから、"msg": "All assertions passed"でチェックに合格しています。
[centos@localhost ansible]$ ansible-playbook -i inventory playbook1.yml
PLAY [cisco] *******************************************************************
TASK [run show command on remote devices (before)] *****************************
ok: [192.168.1.200]
TASK [get parsed routing table using textfsm (before)] *************************
ok: [192.168.1.200]
TASK [extract necessary data from routing table (before)] **********************
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:50', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.1.0.0', 'METRIC': u'130816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.201', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:50', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.1.1.0', 'METRIC': u'130816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.201', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:50', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.1.2.0', 'METRIC': u'130816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.201', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:50', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.1.3.0', 'METRIC': u'130816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.201', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:50', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.1.4.0', 'METRIC': u'130816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.201', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:15', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.2.0.0', 'METRIC': u'2816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.202', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:12', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.2.1.0', 'METRIC': u'2816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.202', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:09', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.2.2.0', 'METRIC': u'2816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.202', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:06', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.2.3.0', 'METRIC': u'2816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.202', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'90', 'UPTIME': u'02:27:03', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'10.2.4.0', 'METRIC': u'2816', 'MASK': u'24', 'NEXTHOP_IP': u'192.168.1.202', 'PROTOCOL': u'D', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'', 'UPTIME': u'', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'192.168.1.0', 'METRIC': u'', 'MASK': u'24', 'NEXTHOP_IP': u'', 'PROTOCOL': u'C', 'TYPE': u''})
ok: [192.168.1.200] => (item={'DISTANCE': u'', 'UPTIME': u'', 'NEXTHOP_IF': u'GigabitEthernet1', 'NETWORK': u'192.168.1.200', 'METRIC': u'', 'MASK': u'32', 'NEXTHOP_IP': u'', 'PROTOCOL': u'L', 'TYPE': u''})
TASK [sort extracted route (before)] *******************************************
ok: [192.168.1.200]
TASK [debug (before)] **********************************************************
ok: [192.168.1.200] => {
"msg": [
[
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "130816",
"NETWORK": "10.1.0.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:50"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "130816",
"NETWORK": "10.1.1.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:50"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "130816",
"NETWORK": "10.1.2.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:50"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "130816",
"NETWORK": "10.1.3.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:50"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "130816",
"NETWORK": "10.1.4.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.201",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:50"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "2816",
"NETWORK": "10.2.0.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:15"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "2816",
"NETWORK": "10.2.1.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:12"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "2816",
"NETWORK": "10.2.2.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:09"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "2816",
"NETWORK": "10.2.3.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:06"
},
{
"DISTANCE": "90",
"MASK": "24",
"METRIC": "2816",
"NETWORK": "10.2.4.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "192.168.1.202",
"PROTOCOL": "D",
"TYPE": "",
"UPTIME": "02:27:03"
},
{
"DISTANCE": "",
"MASK": "24",
"METRIC": "",
"NETWORK": "192.168.1.0",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "",
"PROTOCOL": "C",
"TYPE": "",
"UPTIME": ""
},
{
"DISTANCE": "",
"MASK": "32",
"METRIC": "",
"NETWORK": "192.168.1.200",
"NEXTHOP_IF": "GigabitEthernet1",
"NEXTHOP_IP": "",
"PROTOCOL": "L",
"TYPE": "",
"UPTIME": ""
}
],
[
[
"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"
]
]
]
}
~ 事後確認は省略 ~
TASK [assert that before and ater routing tables are the same] *****************
ok: [192.168.1.200] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY RECAP *********************************************************************
192.168.1.200 : ok=11 changed=0 unreachable=0 failed=0
[centos@localhost ansible]$
終わりに
今回はルーティング確認でしたが、textFSMのテンプレートは他にも様々なものが提供されていますので、応用が利くと思います。
GitHub networktocode/ntc-templates
「ここをこうした方がもっとシンプルだ!」等コメントあれば頂けると嬉しいです。