Ansibleのparse_genieフィルターと通信確認表を使ったNWテスト

# はじめに

以前の記事で、Ansibleのparse_cli_textfsmフィルターを使ったNWテスト例をご紹介しました。
[Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ③通信確認表によるチェック](https://qiita.com/tech_kitara/items/c5ed1379e8f89c574116)

そもそも`parse_cli_textfsm`とは、textFSMというパーサーを使って、テキストデータから必要な値を抽出するフィルターです。
NW機器のshowコマンド用テンプレート（パースするためのルールを定義したファイル）としては、Network to Codeが提供する[NTC-Templates](https://github.com/networktocode/ntc-templates)が有名です。
2019年5月時点で300程度のパーサーが存在し、Cisco、Juniper、Arista、Paloalto、Brocade、Checkpointなどに対応しています。

一方2019年4月末に、同じくパーサー系のフィルターとして`parse_genie`がAnsible Galaxyで公開されました。
[Ansible Galaxy - parse_genie](https://galaxy.ansible.com/clay584/parse_genie)、[GitHub - clay584/parse_genie](https://github.com/clay584/parse_genie)
これは、シスコシステムズが開発したテスト自動化ツール[Genie](https://pubhub.devnetcloud.com/media/pyats-packages/docs/genie/index.html)のパーサーを、Ansible内のカスタムフィルターとして使えるようにしたものです。
2019年5月時点で500を超えるパーサーが存在し、Cisco IOS、IOS-XE、IOS-XR、NX-OS、Junosの各種showコマンドに対応しています。
[Genie - パーサー一覧](https://pubhub.devnetcloud.com/media/pyats-packages/docs/genie/genie_libs/#/parsers)

NTC-Templatesと比較すると、多くのCisco機器のshowコマンドをサポートし、かつ1つのshowコマンドから抽出できるパラメーターも豊富です。
例えば、IOSの`show interfaces`コマンドの場合、NTC-Templatesではカウンター系のパラメーターが抽出できないのに対し、Genie Parserでは取得可能だったりします。

本記事では、`show interfaces`コマンドを例に、通信確認表の項目をAnsible `parse_genie`フィルターを使ってテストする例をご紹介します。

# セットアップ

詳細は[こちら](https://github.com/clay584/parse_genie#prerequisites)を参照願います。

### 前提条件
- Python 3.4以上
- Ansible 2.7以上（他の要件を満たしていれば、古いバージョンでも動くはずとのこと）
- pyATS/Genieパッケージのインストール

今回Pythonは3.6.7を使用しました。
Ansibleは、通信確認表の読み込みで、2.8で追加された`read_csv`モジュールを使うため、2.8.0をインストールしました。


### インストール

(1) 新規ディレクトリの作成と移動

```
[centos@localhost ~]$ mkdir network_ops
[centos@localhost ~]$ cd network_ops
```

(2) Python仮想環境の作成とアクティベート

```
[centos@localhost network_ops]$ python3.6 -m venv venv
[centos@localhost network_ops]$ source venv/bin/activate
```

(3) AnsibleとGenieのインストール

```
(venv) [centos@localhost network_ops]$ sudo pip install ansible
(venv) [centos@localhost network_ops]$ sudo pip install paramiko  # Ansible2.8以降、paramikoは個別インストール
(venv) [centos@localhost network_ops]$ sudo pip install genie
```

(4) Ansible Galaxyからparse_genieロールをインストール

```
(venv) [centos@localhost network_ops]$ ansible-galaxy install clay584.parse_genie
```

# 通信確認表
以下のCSVファイルを作成し、`network_ops`ディレクトリに格納しました。
interface_check_sheet.csv
![無題C111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/3e7c951f-8339-2f2d-8b0a-101596927f1b.png)

各項目の説明は以下の通りです。

- `host`：対象のホスト名
- `interface`： インターフェース名
- `line_protocol`、`oper_status`： 想定のup/downステータス
- `port_speed`、`duplex_mode`： ネゴシエーション後のSpeed、Duplex。Loopback等の論理IFは-でチェック対象外に。
- `in_pkts`、`out_pkts`： in/out方向のパケットカウント。oはチェック対象、-はチェック対象外。
- `in_errors`、`out_errors`： in/out方向のエラーカウント。oはチェック対象、-はチェック対象外。


# Inventoryファイル
簡単のため、確認対象の2台の内、`test3`のみテストします。機種はCisco CSR1000V(IOS-XE)です。

```yaml:inventory
[cisco]
test3 ansible_user=test3 ansible_password=cisco ansible_become_pass=test3

[cisco:vars]
ansible_network_os=ios
ansible_become=yes
ansible_become_method=enable
```

# Playbook
大まかな流れは以下の通りです。

**＜通信確認表の読み込み＞**
　(1) `read_csv`モジュールで、CSV形式の通信確認表をリスト形式で出力
　(2) (1)の結果をdebugで出力
**＜事前ログの取得＞**
　(3) `ios_command`モジュールで、show interfacesコマンドを実行
　(4) parse_genieロールの読み込み
　(5) パース ＆ `set_fact`モジュールで、パース結果を変数に格納
　(6) (5)の結果をdebugで出力
**＜Pingテスト＞**
　(7) パケットカウントをアップさせるため、`ios_ping`モジュールでPingを20回実施
**＜事後ログの取得＞**
　(8)～(10) 事前ログと同じ
**＜通信確認＞**
　(11) `assert`モジュールで以下を確認
　　(A) up/downステータスが想定通りか、通信確認表と事後ログを比較確認
　　(B) Speed、Duplexが想定通りか、通信確認表と事後ログを比較確認
　　(C) パケットカウントアップが見られるか、事前ログと事後ログを比較確認
　　(D) エラーカウントがゼロか、事後ログを確認
　　※(B)～(D)は、チェック対象外（-）とした場合も、便宜的にアサーションがSuccessとなるようにしています。

※(7)、(11)は、Failしても継続処理できるよう、`ignore_errors: yes`を設定。

```yaml:playbook1_interface4.yml
---

- hosts: cisco
  gather_facts: no
  connection: network_cli

  tasks:
    - name: read check sheet data and return a list #(1)
      read_csv:
        path: interface_check_sheet.csv
      register: intf_intended

    - name: debug (before) #(2)
      debug:
        msg: "{{ intf_intended }}"

    - name: run show command on remote devices (before) #(3)
      ios_command:
        commands:
          - show interfaces
      register: result_before

    - name: Read in parse_genie role #(4)
      include_role:
        name: clay584.parse_genie

    - name: get parsed data using parse_genie (before) #(5)
      set_fact:
        intf_before: "{{ result_before.stdout[0] | parse_genie(command='show interfaces', os='iosxe') }}"

    - name: debug (before) #(6)
      debug:
        msg: "{{ intf_before }}"

    - name: Test reachability #(7)
      ios_ping:
        dest: "{{ ping_dest }}"
        count: 20
      ignore_errors: yes

    - name: run show command on remote devices (after) #(8)
      ios_command:
        commands:
          - show interfaces
      register: result_after

    - name: get parsed data using parse_genie (after) #(9)
      set_fact:
        intf_after: "{{ result_after.stdout[0] | parse_genie(command='show interfaces', os='iosxe') }}"

    - name: debug (after) #(10)
      debug:
        msg: "{{ intf_after }}"

    - name: (intf check) interface status #(11)
      assert:
        that:
          #(A) up/down status is as expected (mandatory)
          - intf_after.{{ item.interface }}.line_protocol == '{{ item.line_protocol }}'
          - intf_after.{{ item.interface }}.oper_status == '{{ item.oper_status }}'
          #(B) speed/duplex status is as expected (arbitrary)
          - "item.port_speed == '-' or intf_after.{{ item.interface }}.port_speed == '{{ item.port_speed }}'"
          - "item.duplex_mode == '-' or intf_after.{{ item.interface }}.duplex_mode == '{{ item.duplex_mode }}'"
          #(C) increase in packet counts (arbitrary)
          - "item.in_pkts == '-' or intf_before.{{ item.interface }}.counters.in_pkts < intf_after.{{ item.interface }}.counters.in_pkts"
          - "item.out_pkts == '-' or intf_before.{{ item.interface }}.counters.out_pkts < intf_after.{{ item.interface }}.counters.out_pkts"
          #(D) no error counts (arbitrary)
          - "item.in_errors == '-' or intf_after.{{ item.interface }}.counters.in_errors == 0"
          - "item.out_errors == '-' or intf_after.{{ item.interface }}.counters.out_errors == 0"
      when: item.host == inventory_hostname
      loop: "{{ intf_intended.list | flatten(levels=1) }}"
      ignore_errors: yes

  vars:
    ping_dest: 192.168.100.1
```

# 実行結果①（成功時）

最後のTASK [(intf check) interface status] で、想定通り`test3`の3インターフェースの結果が`ok`となり、`test4`は`skipping`されている事が分かります。

```yaml:
(venv) [centos@localhost network_ops]$ ansible-playbook -i inventory playbook1_interface4.yml

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

TASK [read check sheet data and return a list] ******************************************************************
ok: [test3]

TASK [debug (before)] *******************************************************************************************
ok: [test3] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": false,
        "dict": {},
        "failed": false,
        "list": [
            {
                "duplex_mode": "full",
                "host": "test3",
                "in_errors": "o",
                "in_pkts": "o",
                "interface": "GigabitEthernet1",
                "line_protocol": "up",
                "oper_status": "up",
                "out_errors": "o",
                "out_pkts": "o",
                "port_speed": "1000"
            },
            {
                "duplex_mode": "-",
                "host": "test3",
                "in_errors": "-",
                "in_pkts": "-",
                "interface": "GigabitEthernet3",
                "line_protocol": "down",
                "oper_status": "down",
                "out_errors": "-",
                "out_pkts": "-",
                "port_speed": "-"
            },
            {
                "duplex_mode": "-",
                "host": "test3",
                "in_errors": "-",
                "in_pkts": "-",
                "interface": "Loopback0",
                "line_protocol": "up",
                "oper_status": "up",
                "out_errors": "-",
                "out_pkts": "-",
                "port_speed": "-"
            },
（test4 省略）
        ]
    }
}

TASK [run show command on remote devices (before)] **************************************************************
ok: [test3]

TASK [Read in parse_genie role] *********************************************************************************

TASK [get parsed data using parse_genie (before)] ***************************************************************
ok: [test3]

TASK [debug (before)] *******************************************************************************************
ok: [test3] => {
    "msg": {
        "GigabitEthernet1": {
            "arp_timeout": "04:00:00",
            "arp_type": "arpa",
            "auto_negotiate": true,
            "bandwidth": 1000000,
            "counters": {
                "in_broadcast_pkts": 0,
                "in_crc_errors": 0,
                "in_errors": 0,
                "in_frame": 0,
                "in_giants": 0,
                "in_ignored": 0,
                "in_mac_pause_frames": 0,
                "in_multicast_pkts": 0,
                "in_no_buffer": 0,
                "in_octets": 5956421,
                "in_overrun": 0,
                "in_pkts": 35741,
                "in_runts": 0,
                "in_throttles": 0,
                "in_watchdog": 0,
                "last_clear": "never",
                "out_babble": 0,
                "out_buffer_failure": 0,
                "out_buffers_swapped": 0,
                "out_collision": 0,
                "out_deferred": 0,
                "out_errors": 0,
                "out_interface_resets": 0,
                "out_late_collision": 0,
                "out_lost_carrier": 0,
                "out_mac_pause_frames": 0,
                "out_no_carrier": 0,
                "out_octets": 3686861,
                "out_pkts": 32808,
                "out_underruns": 0,
                "out_unknown_protocl_drops": 0,
                "rate": {
                    "in_rate": 1000,
                    "in_rate_pkts": 1,
                    "load_interval": 300,
                    "out_rate": 1000,
                    "out_rate_pkts": 1
                }
            },
            "delay": 10,
            "duplex_mode": "full",
            "enabled": true,
            "encapsulations": {
                "encapsulation": "arpa"
            },
            "flow_control": {
                "receive": false,
                "send": false
            },
            "ipv4": {
                "192.168.100.201/24": {
                    "ip": "192.168.100.201",
                    "prefix_length": "24"
                }
            },
            "keepalive": 10,
            "last_input": "00:00:16",
            "last_output": "00:01:06",
            "line_protocol": "up",
            "link_type": "auto",
            "mac_address": "000c.2924.d8a8",
            "media_type": "RJ45",
            "mtu": 1500,
            "oper_status": "up",
            "output_hang": "never",
            "phys_address": "000c.2924.d8a8",
            "port_channel": {
                "port_channel_member": false
            },
            "port_speed": "1000",
            "queues": {
                "input_queue_drops": 0,
                "input_queue_flushes": 0,
                "input_queue_max": 375,
                "input_queue_size": 0,
                "output_queue_max": 40,
                "output_queue_size": 0,
                "queue_strategy": "fifo",
                "total_output_drop": 0
            },
            "reliability": "255/255",
            "rxload": "1/255",
            "txload": "1/255",
            "type": "CSR vNIC"
        },
（test3 GigabitEthernet1以外を省略）
}

TASK [Test reachability] ****************************************************************************************
ok: [test3]

TASK [run show command on remote devices (after)] ***************************************************************
ok: [test3]

TASK [get parsed data using parse_genie (after)] ****************************************************************
ok: [test3]

TASK [debug (after)] ********************************************************************************************
ok: [test3] => 
（省略）

TASK [(intf check) interface status] ****************************************************************************
ok: [test3] => (item={'duplex_mode': 'full', 'line_protocol': 'up', 'in_errors': 'o', 'host': 'test3', 'out_pkts': 'o', 'oper_status': 'up', 'port_speed': '1000', 'interface': 'GigabitEthernet1', 'in_pkts': 'o', 'out_errors': 'o'}) => {
    "ansible_loop_var": "item",
    "changed": false,
    "item": {
        "duplex_mode": "full",
        "host": "test3",
        "in_errors": "o",
        "in_pkts": "o",
        "interface": "GigabitEthernet1",
        "line_protocol": "up",
        "oper_status": "up",
        "out_errors": "o",
        "out_pkts": "o",
        "port_speed": "1000"
    },
    "msg": "All assertions passed"
}
ok: [test3] => (item={'duplex_mode': '-', 'line_protocol': 'down', 'in_errors': '-', 'host': 'test3', 'out_pkts': '-', 'oper_status': 'down', 'port_speed': '-', 'interface': 'GigabitEthernet3', 'in_pkts': '-', 'out_errors': '-'}) => {
    "ansible_loop_var": "item",
    "changed": false,
    "item": {
        "duplex_mode": "-",
        "host": "test3",
        "in_errors": "-",
        "in_pkts": "-",
        "interface": "GigabitEthernet3",
        "line_protocol": "down",
        "oper_status": "down",
        "out_errors": "-",
        "out_pkts": "-",
        "port_speed": "-"
    },
    "msg": "All assertions passed"
}
ok: [test3] => (item={'duplex_mode': '-', 'line_protocol': 'up', 'in_errors': '-', 'host': 'test3', 'out_pkts': '-', 'oper_status': 'up', 'port_speed': '-', 'interface': 'Loopback0', 'in_pkts': '-', 'out_errors': '-'}) => {
    "ansible_loop_var": "item",
    "changed": false,
    "item": {
        "duplex_mode": "-",
        "host": "test3",
        "in_errors": "-",
        "in_pkts": "-",
        "interface": "Loopback0",
        "line_protocol": "up",
        "oper_status": "up",
        "out_errors": "-",
        "out_pkts": "-",
        "port_speed": "-"
    },
    "msg": "All assertions passed"
}
skipping: [test3] => (item={'duplex_mode': 'full', 'line_protocol': 'up', 'in_errors': 'o', 'host': 'test4', 'out_pkts': 'o', 'oper_status': 'up', 'port_speed': '1000', 'interface': 'GigabitEthernet1', 'in_pkts': 'o', 'out_errors': 'o'}) 
skipping: [test3] => (item={'duplex_mode': '-', 'line_protocol': 'down', 'in_errors': '-', 'host': 'test4', 'out_pkts': '-', 'oper_status': 'down', 'port_speed': '-', 'interface': 'GigabitEthernet3', 'in_pkts': '-', 'out_errors': '-'}) 
skipping: [test3] => (item={'duplex_mode': '-', 'line_protocol': 'up', 'in_errors': '-', 'host': 'test4', 'out_pkts': '-', 'oper_status': 'up', 'port_speed': '-', 'interface': 'Loopback0', 'in_pkts': '-', 'out_errors': '-'}) 

PLAY RECAP ******************************************************************************************************
test3                      : ok=10   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

```

# 実行結果②（失敗時）
通信確認表の想定結果を、あえて誤った値にしてみます。

- `GigabitEthernet1`の`port_speed`を、`1000` ⇒`100`に
- `GigabitEthernet3`の`line_protocol` / `oper_status`を、`down` / `down`から`up` / `up`に
![無題C1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/844e4808-9aad-46ae-7ff5-4024d417397a.png)

該当インターフェースの結果が`failed`になりました。Failは無視されるので、最後の実行結果では`ignored`でカウントされています。

```yaml:
（省略）
TASK [(intf check) interface status] ****************************************************************************
（省略）
failed: [test3] (item={'duplex_mode': 'full', 'line_protocol': 'up', 'in_errors': 'o', 'host': 'test3', 'out_pkts': 'o', 'oper_status': 'up', 'port_speed': '100', 'interface': 'GigabitEthernet1', 'in_pkts': 'o', 'out_errors': 'o'}) => {
    "ansible_loop_var": "item",
    "assertion": "item.port_speed == '-' or intf_after.GigabitEthernet1.port_speed == '100'",
    "changed": false,
    "evaluated_to": false,
    "item": {
        "duplex_mode": "full",
        "host": "test3",
        "in_errors": "o",
        "in_pkts": "o",
        "interface": "GigabitEthernet1",
        "line_protocol": "up",
        "oper_status": "up",
        "out_errors": "o",
        "out_pkts": "o",
        "port_speed": "100"
    },
    "msg": "Assertion failed"
}
failed: [test3] (item={'duplex_mode': '-', 'line_protocol': 'up', 'in_errors': '-', 'host': 'test3', 'out_pkts': '-', 'oper_status': 'up', 'port_speed': '-', 'interface': 'GigabitEthernet3', 'in_pkts': '-', 'out_errors': '-'}) => {
    "ansible_loop_var": "item",
    "assertion": "intf_after.GigabitEthernet3.line_protocol == 'up'",
    "changed": false,
    "evaluated_to": false,
    "item": {
        "duplex_mode": "-",
        "host": "test3",
        "in_errors": "-",
        "in_pkts": "-",
        "interface": "GigabitEthernet3",
        "line_protocol": "up",
        "oper_status": "up",
        "out_errors": "-",
        "out_pkts": "-",
        "port_speed": "-"
    },
    "msg": "Assertion failed"
}
（省略）
PLAY RECAP ******************************************************************************************************
test3                      : ok=10   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1  
```

# 所感
今回改めて、Genie Parserがいかに強力かを認識できました。Cisco機器のログ取得、設定変更、テストをすべてAnsibleで行う場合、個人的には欠かせない選択肢となりそうです。
この方法を応用すれば、他の設定項目のテストもAnsibleで自動化できると思います。ただ、複数項目をチェックする場合は、Playbookが膨大になるので、今後設計項目毎にロール化して行ければと考えています。