LoginSignup
9
8

More than 3 years have passed since last update.

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

Posted at

はじめに

以前の記事で、Ansibleのparse_cli_textfsmフィルターを使ったNWテスト例をご紹介しました。
Ansibleのparse_cli_textfsmフィルターを使ったNWテスト ③通信確認表によるチェック

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

一方2019年4月末に、同じくパーサー系のフィルターとしてparse_genieがAnsible Galaxyで公開されました。
Ansible Galaxy - parse_genieGitHub - clay584/parse_genie
これは、シスコシステムズが開発したテスト自動化ツールGenieのパーサーを、Ansible内のカスタムフィルターとして使えるようにしたものです。
2019年5月時点で500を超えるパーサーが存在し、Cisco IOS、IOS-XE、IOS-XR、NX-OS、Junosの各種showコマンドに対応しています。
Genie - パーサー一覧

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

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

セットアップ

詳細はこちらを参照願います。

前提条件

  • 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

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

  • host:対象のホスト名
  • interface: インターフェース名
  • line_protocoloper_status: 想定のup/downステータス
  • port_speedduplex_mode: ネゴシエーション後のSpeed、Duplex。Loopback等の論理IFは-でチェック対象外に。
  • in_pktsout_pkts: in/out方向のパケットカウント。oはチェック対象、-はチェック対象外。
  • in_errorsout_errors: in/out方向のエラーカウント。oはチェック対象、-はチェック対象外。

Inventoryファイル

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

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を設定。

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となり、test4skippingされている事が分かります。

(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   

実行結果②(失敗時)

通信確認表の想定結果を、あえて誤った値にしてみます。

  • GigabitEthernet1port_speedを、1000100
  • GigabitEthernet3line_protocol / oper_statusを、down / downからup / up無題C1.png

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

(省略)
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が膨大になるので、今後設計項目毎にロール化して行ければと考えています。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8