はじめに
以前の記事で、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_genie、GitHub - 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
各項目の説明は以下の通りです。
-
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)です。
[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
を設定。
---
- 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
されている事が分かります。
(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
に
該当インターフェースの結果が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が膨大になるので、今後設計項目毎にロール化して行ければと考えています。