Ansibleのparse_cli_textfsmフィルターを使ったネットワークテストの自動化

# はじめに

Ansibleの`parse_cli_textfsm`フィルター、`set_fact`モジュール、`assert`モジュールを組み合わせると、CLIコマンド出力結果を元に様々なネットワークテストができます。
今回は、Cisco IOS RouterのEIGRPネイバーテストをAnsibleで自動化してみました。

# 概要

### parse_cli_textfsmフィルターとは

- Ansibleの[ネットワークCLIフィルタープラグイン](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html?highlight=filter#network-cli-filters)の1つ。
- TextFSMライブラリを使ってCLIコマンド出力結果をパースする。
- 既存のTextFSMライブラリとして、networktocodeのGitHubリポジトリで公開されている[NTC-Templates](https://github.com/networktocode/ntc-templates)があります。Cisco IOSの場合、サポートしているshowコマンドは2018年11月時点で53種類。(ちなみにshow ip eigrp neighborsは私がプルリクエストを出したものです。)
- インストール方法は、[てくなべ (tekunabe)さんのブログ](https://tekunabe.hatenablog.jp/entry/2017/09/23/parse_cli_textfsm)が参考になると思います。

### set_factモジュールとは

- 変数を格納するためのモジュール。詳細は[公式ドキュメント](https://docs.ansible.com/ansible/2.7/modules/set_fact_module.html?highlight=set_fact)を参照願います。

### assertモジュールとは

- True/False判定のためのモジュール。例えば、ある2つの変数が同じ値か、部分集合の関係になっているか、特定の値以上かを判定可能。詳細は[公式ドキュメント](https://docs.ansible.com/ansible/2.7/modules/assert_module.html?highlight=assert)を参照願います。

# ラボ環境

以下の仮想ルータ(Cisco CSR1000V)を同一セグメントに接続し、互いにEIGRPネイバーを形成するようにしました。

| 機種       |       IPアドレス |    備考   |
|:-----------------|:------------------|:------------------|
| test　| 192.168.100.200 ||
| test2　| 192.168.100.201 ||
| csr1　| 192.168.100.202 | Ansibleによるテスト対象 |

showコマンド出力結果は以下の通りです。

```text:
csr1#show ip eigrp neighbors
EIGRP-IPv4 Neighbors for AS(10)
H   Address                 Interface              Hold Uptime   SRTT   RTO  Q  Seq
                                                   (sec)         (ms)       Cnt Num
1   192.168.100.201         Gi1                      12 01:07:01    6   100  0  19
0   192.168.100.200         Gi1                      12 01:07:01   32   192  0  20
```

# Playbook

例として、EIGRPの以下2点を確認するPlaybookを作成しました。
　①想定のEIGRPネイバーが存在するか
　②全ネイバーのUPTIMEが1分より大きいか
※変数はPlaybook内に記載しましたが、メンテナンスの観点からInventoryやVariableファイルに書いた方がいいと思います。

```yaml:playbook2_eigrp.yml
---

- hosts: cisco
  gather_facts: no
  connection: local

  tasks:
    - name: run show command on remote devices
      ios_command:
        commands:
          - show ip eigrp neighbors   # show ip eigrp neighborコマンドを出力。
        provider: "{{ cli }}"
      register: result

    - name: set the actual eigrp neighbor value (all)
      set_fact:
        actual_eigrp_nei: "{{ result.stdout[0] | parse_cli_textfsm('./ntc-templates-master/templates/cisco_ios_show_ip_eigrp_neighbors.template') }}"
        # show ip eigrp neighbors用のTemplateファイルを使って出力結果をパースし、
        # リスト/辞書データとしてactual_eigrp_neiに格納。

    - name: set the actual eigrp neighbor value (address)
      set_fact:
        actual_eigrp_nei_address: "{{ actual_eigrp_nei_address + [item.ADDRESS] }}"
      loop: "{{ actual_eigrp_nei|flatten(levels=1) }}"
      # loopでactual_eigrp_nei内のリストデータを1つずつ読み込み、key=ADDRESSに格納されている
      # IPアドレスを、actual_eigrp_nei_addressにAppend。

    - name: debug
      debug:
        msg:
          - "{{ actual_eigrp_nei }}"   # actual_eigrp_neiに格納されているデータを確認
          - "{{ actual_eigrp_nei_address }}"   # actual_eigrp_nei_addressに格納されているデータを確認

    - name: assert that expected neighbors are in actual neighbors
      assert:
        that:
          - "item in actual_eigrp_nei_address"
      loop: "{{ expected_eigrp_nei_address|flatten(levels=1) }}"
      # loopでexpected_eigrp_nei_addressのリストを1つずつ読み込み、その値がactual_eigrp_nei_addressに含まれるか確認。

    - name: assert that all neighbors are up for more than 1 minute
      assert:
        that:
          - "expected_eigrp_nei_uptime < item.UPTIME"
      loop: "{{ actual_eigrp_nei|flatten(levels=1) }}"
      # loopでactual_eigrp_neiのリストを1つずつ読み込み、key=UPTIMEの値が想定時間(1分)より大きいか確認。

  vars:
    cli:
      host: "{{ inventory_hostname }}"
      username: "{{ ansible_username }}"
      password: "{{ ansible_password }}"
      authorize: true
      auth_pass: "{{ enable_secret }}"
    expected_eigrp_nei_address: ["192.168.100.200", "192.168.100.201"]   # 想定のEIGRPネイバーアドレス
    # リスト形式の空データを作成。loop処理でリスト内にデータをAppendするための元データとして必要。
    actual_eigrp_nei_address: []
    expected_eigrp_nei_uptime: 00:01:00   # 想定のEIGRP Uptime
```

#実行結果(成功例)

①②ともに成功し、いずれも"All assertions passed"となっています。

```yaml:
[<ユーザ名>@localhost ansible]$ ansible-playbook -i inventory playbook2_eigrp.yml

PLAY [cisco] ********************************************************************************

TASK [run show command on remote devices] ***************************************************
ok: [192.168.100.202]

TASK [set the actual eigrp neighbor value (all)] ********************************************
ok: [192.168.100.202]

TASK [set the actual eigrp neighbor value (address)] ****************************************
ok: [192.168.100.202] => (item={'Q_CNT': u'0', 'UPTIME': u'01:43:53', 'SEQ_NUM': u'19', 'RTO': u'100', 'SRTT': u'6', 'AS': u'10', 'ADDRESS': u'192.168.100.201', 'INTERFACE': u'Gi1', 'HOLD': u'13'})
ok: [192.168.100.202] => (item={'Q_CNT': u'0', 'UPTIME': u'01:43:53', 'SEQ_NUM': u'20', 'RTO': u'192', 'SRTT': u'32', 'AS': u'10', 'ADDRESS': u'192.168.100.200', 'INTERFACE': u'Gi1', 'HOLD': u'12'})

TASK [debug] ********************************************************************************
ok: [192.168.100.202] => {
    "msg": [
        [
            {
                "ADDRESS": "192.168.100.201", 
                "AS": "10", 
                "HOLD": "13", 
                "INTERFACE": "Gi1", 
                "Q_CNT": "0", 
                "RTO": "100", 
                "SEQ_NUM": "19", 
                "SRTT": "6", 
                "UPTIME": "01:43:53"
            }, 
            {
                "ADDRESS": "192.168.100.200", 
                "AS": "10", 
                "HOLD": "12", 
                "INTERFACE": "Gi1", 
                "Q_CNT": "0", 
                "RTO": "192", 
                "SEQ_NUM": "20", 
                "SRTT": "32", 
                "UPTIME": "01:43:53"
            }
        ], 
        [
            "192.168.100.201", 
            "192.168.100.200"
        ]
    ]
}

TASK [assert that expected neighbors are in actual neighbors] *******************************
ok: [192.168.100.202] => (item=192.168.100.200) => {
    "changed": false, 
    "item": "192.168.100.200", 
    "msg": "All assertions passed"   # 想定の"192.168.100.200"が実際の出力結果に含まれており、"All assertions passed"。
}
ok: [192.168.100.202] => (item=192.168.100.201) => {
    "changed": false, 
    "item": "192.168.100.201", 
    "msg": "All assertions passed"   # 想定の"192.168.100.201"が実際の出力結果に含まれており、"All assertions passed"。
}

TASK [assert that all neighbors are up for more than 1 minute] ******************************
ok: [192.168.100.202] => (item={'Q_CNT': u'0', 'UPTIME': u'01:43:53', 'SEQ_NUM': u'19', 'RTO': u'100', 'SRTT': u'6', 'AS': u'10', 'ADDRESS': u'192.168.100.201', 'INTERFACE': u'Gi1', 'HOLD': u'13'}) => {
    "changed": false, 
    "item": {
        "ADDRESS": "192.168.100.201", 
        "AS": "10", 
        "HOLD": "13", 
        "INTERFACE": "Gi1", 
        "Q_CNT": "0", 
        "RTO": "100", 
        "SEQ_NUM": "19", 
        "SRTT": "6", 
        "UPTIME": "01:43:53"
    }, 
    "msg": "All assertions passed"   # "192.168.100.201"のUPTIMEは1分以上であり、"All assertions passed"。
}
ok: [192.168.100.202] => (item={'Q_CNT': u'0', 'UPTIME': u'01:43:53', 'SEQ_NUM': u'20', 'RTO': u'192', 'SRTT': u'32', 'AS': u'10', 'ADDRESS': u'192.168.100.200', 'INTERFACE': u'Gi1', 'HOLD': u'12'}) => {
    "changed": false, 
    "item": {
        "ADDRESS": "192.168.100.200", 
        "AS": "10", 
        "HOLD": "12", 
        "INTERFACE": "Gi1", 
        "Q_CNT": "0", 
        "RTO": "192", 
        "SEQ_NUM": "20", 
        "SRTT": "32", 
        "UPTIME": "01:43:53"
    }, 
    "msg": "All assertions passed"   # "192.168.100.200"のUPTIMEは1分以上であり、"All assertions passed"。
}

PLAY RECAP **********************************************************************************
192.168.100.202            : ok=6    changed=0    unreachable=0    failed=0   
```

#実行結果(失敗例)

EIGRPネイバーをリセットし、UPTIME1分前後の結果を確認しました。
期待通り、UPTIMEが1分ちょうどの時は"Assertion failed"、1分を超えた時は"All assertions passed"となっていることが分かります。

```yaml:
[<ユーザ名>@localhost ansible]$ ansible-playbook -i inventory playbook2_eigrp.yml -vvv
---<中略>---
TASK [assert that all neighbors are up for more than 1 minute] ******************************
task path: /home/<ユーザ名>/ansible/playbook2_eigrp.yml:58
failed: [192.168.100.202] (item={'Q_CNT': u'0', 'UPTIME': u'00:01:00', 'SEQ_NUM': u'19', 'RTO': u'100', 'SRTT': u'6', 'AS': u'10', 'ADDRESS': u'192.168.100.201', 'INTERFACE': u'Gi1', 'HOLD': u'14'}) => {
    "assertion": "expected_eigrp_nei_uptime < item.UPTIME", 
    "changed": false, 
    "evaluated_to": false, 
    "item": {
        "ADDRESS": "192.168.100.201", 
        "AS": "10", 
        "HOLD": "14", 
        "INTERFACE": "Gi1", 
        "Q_CNT": "0", 
        "RTO": "100", 
        "SEQ_NUM": "19", 
        "SRTT": "6", 
        "UPTIME": "00:01:00"
    }, 
    "msg": "Assertion failed"   # UPTIMEは1分で、想定の"1分より大きい"を満たしておらず"Assertion failed"。
}
ok: [192.168.100.202] => (item={'Q_CNT': u'0', 'UPTIME': u'00:01:01', 'SEQ_NUM': u'20', 'RTO': u'192', 'SRTT': u'32', 'AS': u'10', 'ADDRESS': u'192.168.100.200', 'INTERFACE': u'Gi1', 'HOLD': u'14'}) => {
    "changed": false, 
    "item": {
        "ADDRESS": "192.168.100.200", 
        "AS": "10", 
        "HOLD": "14", 
        "INTERFACE": "Gi1", 
        "Q_CNT": "0", 
        "RTO": "192", 
        "SEQ_NUM": "20", 
        "SRTT": "32", 
        "UPTIME": "00:01:01"
    }, 
    "msg": "All assertions passed"   # UPTIMEは1分1秒で、想定結果を満たしているため"All assertions passed"。
}
	to retry, use: --limit @/home/<ユーザ名>/ansible/playbook2_eigrp.retry

PLAY RECAP **********************************************************************************
192.168.100.202            : ok=5    changed=0    unreachable=0    failed=1 
```