Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ③通信確認表によるチェック

# はじめに

前回・前々回に続き、今回は、通信確認表で定義した想定結果と、実際の結果が同じか、Ansibleでチェックする方法をご紹介したいと思います。

前々回：[Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ①事前事後の無影響確認](https://qiita.com/tech_kitara/private/d30b3219e8362d7592af)
前回：[Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ②事前事後の差分確認](https://qiita.com/tech_kitara/private/42b108df7a7aa1f42dcd)

おそらくテストパターンの中で自動化が一番面倒で、実現方法も色々とあると思います。
今回はAnsible2.8で登場予定の`read_csv`モジュールを使い、CSV形式の通信確認表をリスト形式に変換し、同じくリスト形式にした実際の結果と比較してみます。

# 通信確認表
ホスト名csr1、csr2の機器を新規導入する想定で、ルーティングテーブルの確認表を作成しました。
`HOST`は確認対象のホスト名、`ADD_DEL`はルート追加・削除、`PROTOCOL`はEIGRP(D)や直接接続(C)、ローカル(L)、`NETWORK`/`MASK`は宛先ネットワーク、`NEXTHOP_IP`はネクストホップIPアドレス、`RESULT`は結果を記載する欄になります。

今回は簡単のために、csr1のみチェックしてみます。

route_check_sheet.csv
![CSV.png](https://qiita-image-store.s3.amazonaws.com/0/310082/83312874-4782-57e1-937e-3e9a3068930d.png)

# Inventory
AnsibleをインストールしたLinux上で、csr1をhosts登録しています。

```yaml:inventory1
[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)は前回、前々回のものを流用しています。


```yaml:playbook3.yml
---

- 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"となっています。

```yaml:
[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ディレクトリに格納して呼び出す必要があります。