Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ①事前事後の無影響確認

# はじめに
Ansibleの`parse_cli_textfsm`フィルター、`set_fact`モジュール、`assert`モジュールを組み合わせると、CLIコマンド出力結果を元に様々なネットワークテストができます。

以前Cisco IOSで、EIGRPのネイバーが存在するか、UPTIMEが1分より大きいか確認するPlaybookをご紹介しました。
[Ansibleのparse_cli_textfsmフィルターを使ったネットワークテストの自動化](https://qiita.com/tech_kitara/items/0a9df7ce38c96e90f3f0)

上記は想定結果を指定するパターンですが、それ以外にも、事前事後でステータスが変わらないことを確認する場面も良くあるかと思います。
(NW機器のリプレイス、OSアップグレード、その他小規模メンテナンス作業等)

今回はCisco IOSの`show ip route`コマンドで表示される宛先、ネクストホップ、プロトコルが、事前事後で変わらないことを確認するPlaybookを作成してみました。

# Inventory
3台のCisco CSR1000Vを用意し、お互いにEIGRPでルーティング情報をやりとりする構成にしています。
確認結果をシンプルにするため、Inventory上は1台のみ指定しています。

```text:inventory
[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アップグレードなどを行うイメージです。

```yaml:playbook.yml
---

- 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"でチェックに合格しています。

```yaml:
[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](https://github.com/networktocode/ntc-templates)

「ここをこうした方がもっとシンプルだ！」等コメントあれば頂けると嬉しいです。