てくなべ (tekunabe)

ansible / network / automation / StackStorm

show コマンド結果をパースする方法あれこれ(TextFSM / Genie Parser と Netmiko / Ansible の組み合わせ)

2019/09/05 に、ネットワークプログラマビリティ勉強会 #18で、「show コマンド結果をパースする方法あれこれ」という発表をさせていだきました。サンプルコードが中心だったため、コピペしやすいように、ブログ記事として書きおこします。



■ 1. はじめに

ネットワーク機器の通常の show コマンドの結果は、機械(プログラム)にとっては取り扱いにくくなりがちです。これを解決する、show コマンドの結果を構造化データ(JSONなど)にするパーサーをご紹介します。

具体的には、TextFSMGenie Parser という2つのパーサーです。また、自動化ツール NetmikoAnsible との組み合わせも、サンブルコードベースにご紹介します。すべて Python ベースのものです。

f:id:akira6592:20190906175638p:plain
パーサーと自動化ツールの関係

「そもそもネットワーク器機が構造化データを出力してくれればいいのでは」と思われるとも思いますが、運用中の機器にはそういった機能なまだまだ無かったりすることも多いのではないでしょうか。そんなときに、これらのパーサーが役に立ちます。


■ 2. パーサーとは

パーサーとは、show コマンドの結果を機械(プログラム)が取り扱いやすい構造化データに変換するものです。(今回の記事の文脈上の説明)

例えば、以下のような show ip interface brief の結果

csr1000v#show ip interface brief
Interface          IP-Address      OK? Method Status                Protocol
GigabitEthernet1   10.10.20.48     YES NVRAM  up                    up      
GigabitEthernet2   unassigned      YES NVRAM  administratively down down    
GigabitEthernet3   unassigned      YES NVRAM  administratively down down    

を、以下のような機械が取り扱いやすいデータに変換します。

{
  "INTF": "GigabitEthernet1",
  "IPADDR": "10.10.20.48",
  "PROTO": "up",
  "STATUS": "up"
},
{
  "INTF": "GigabitEthernet2",
  "IPADDR": "unassigned",
  "PROTO": "down",
  "STATUS": "administratively down"
},
{
  "INTF": "GigabitEthernet3",
  "IPADDR": "unassigned",
  "PROTO": "down",
  "STATUS": "administratively down"
}

今回、この記事でパースする、サンプルの show コマンド結果はこちらです。 Cisco DevNet Sandbox CSRV1000V IOS-XE (16.11.01a) の環境を利用させていただきました。

csr1000v#show ip interface brief
Interface            IP-Address      OK? Method Status                Protocol
GigabitEthernet1     10.10.20.48     YES NVRAM  up                    up      
GigabitEthernet2     unassigned      YES NVRAM  administratively down down    
GigabitEthernet3     unassigned      YES NVRAM  administratively down down    
Loopback0            unassigned      YES unset  up                    up      
Loopback1            unassigned      YES unset  up                    up      
Loopback2            unassigned      YES unset  up                    up     


■ 3. TextFSM

3.1. TextFSM とは

TextFSM は、カスタマイズが容易なパーサーです。

  • ntc-templates のテンプレートを利用可
    • プラットフォーム数 23
    • 1コマンド数 290
    • (2019/09/03現在)
  • テンプレートは追加、カスタマイズ可能
  • pip install textfsm でインストール

テンプレートは以下のような形です。正規表現でパースする方法を定義します。

Value INTF (\S+)
Value IPADDR (\S+)
Value STATUS (up|down|administratively down)
Value PROTO (up|down)

Start
  ^${INTF}\s+${IPADDR}\s+\w+\s+\w+\s+${STATUS}\s+${PROTO} -> Record

3.2. TextFSM 単体の利用例

TextFSM 単体では、ネットワーク機器への接続機能はありません。そのためは何かしらの方法で取得したコマンド結果を用意しておきます。(コード内の raw_text_data に相当するデータ)

コード

import textfsm
from pprint import pprint

# テンプレートファイルの読み込み
template = open('./templates/cisco_ios_show_ip_interface_brief.template', 'r')
re_table = textfsm.TextFSM(template)

# raw_text_data は何かしらの方法で取得したコマンド結果
fsm_results = re_table.ParseText(raw_text_data)
 
results = list()
# ヘッダーとデータを結合
for item in fsm_results:
    results.append(dict(zip(re_table.header, item)))

# 表示
pprint(results)

なお、公式のサンプルはこちらです。

実行結果

以下のようにパースされます。ここまで、構造化されていれば、コードから取り扱いしやすいですね。

[{'INTF': 'GigabitEthernet1',
  'IPADDR': '10.10.20.48',
  'PROTO': 'up',
  'STATUS': 'up'},
 {'INTF': 'GigabitEthernet2',
  'IPADDR': 'unassigned',
  'PROTO': 'down',
  'STATUS': 'administratively down'},
 {'INTF': 'GigabitEthernet3',
  'IPADDR': 'unassigned',
  'PROTO': 'down',
  'STATUS': 'administratively down'},
 {'INTF': 'Loopback0', 'IPADDR': 'unassigned', 'PROTO': 'up', 'STATUS': 'up'},
 {'INTF': 'Loopback1', 'IPADDR': 'unassigned', 'PROTO': 'up', 'STATUS': 'up'},
 {'INTF': 'Loopback2', 'IPADDR': 'unassigned', 'PROTO': 'up', 'STATUS': 'up'}]

3.3. TextFMS + Netmiko 利用例

ネットワーク自動化ライブラリ Netmikoでは、Netmiko 2.0.0 から TextFSM が同梱されていて、簡単に連携できます。

なお、Netmiko インストール時に TextFSM も一緒にインストールされます。

コード

send_command の引数に、use_textfsm=True を付加するのがポイントです。

from netmiko import Netmiko
import os
from pprint import pprint

# 接続情報の定義
ios1 = {
  "host": "10.10.20.48",
  "device_type": "cisco_ios",
  "username": "testuser",
  "password": "testpass"
}

# テンプレートファイル格納ディレクトリを環境変数に指定
# デフォルトは ./ntc-templates/templates
os.environ["NET_TEXTFSM"] = "/home/npstudy/templates"

# 接続
net_connect = Netmiko(**ios1)
# コマンド実行、パース
result = net_connect.send_command("show ip interface brief", use_textfsm=True)
# 表示
pprint(result)

実行結果

以下のようにパースされます。

[{'intf': 'GigabitEthernet1',
  'ipaddr': '10.10.20.48',
  'proto': 'up',
  'status': 'up'},
 {'intf': 'GigabitEthernet2',
  'ipaddr': 'unassigned',
  'proto': 'down',
  'status': 'administratively down'},
 {'intf': 'GigabitEthernet3',
  'ipaddr': 'unassigned',
  'proto': 'down',
  'status': 'administratively down'},
 {'intf': 'Loopback0', 'ipaddr': 'unassigned', 'proto': 'up', 'status': 'up'},
 {'intf': 'Loopback1', 'ipaddr': 'unassigned', 'proto': 'up', 'status': 'up'},
 {'intf': 'Loopback2', 'ipaddr': 'unassigned', 'proto': 'up', 'status': 'up'}]

3.4. TextFMS + Ansible 利用例

今度は Ansible と組み合わせた利用例です。

Ansible から TextFSM のテンプレートを利用して、パースするには Ansible 2.4 で追加された parse_cli_textfsmフィルターを利用します。

このフィルターを利用するには、あらかじめ TextFSM をインストールしておく必要があります。

$ pip install textfsm

Playbook

- hosts: ios
  gather_facts: no
 
  vars:
    template_file: "./templates/cisco_ios_show_ip_interface_brief.template"
 
  tasks:
    - name: show
      ios_command:
        commands:
          - show ip interface brief
      register: result
 
    - name: show parsed result
      debug:
        msg: "{{ result.stdout[0] | parse_cli_textfsm(template_file) }}"

実行結果

以下のようにパースされます。

ok: [ios1] => {
  "msg": [
      {
          "INTF": "GigabitEthernet1",
          "IPADDR": "10.10.20.48",
          "PROTO": "up",
          "STATUS": "up"
      },
      {
          "INTF": "GigabitEthernet2",
          "IPADDR": "unassigned",
          "PROTO": "down",
          "STATUS": "administratively down"
      },
      {
          "INTF": "GigabitEthernet3",
          "IPADDR": "unassigned",
          "PROTO": "down",
          "STATUS": "administratively down"
      },
      {
          "INTF": "Loopback0",
          "IPADDR": "unassigned",
          "PROTO": "up",
          "STATUS": "up"
      },
      {
          "INTF": "Loopback1",
          "IPADDR": "unassigned",
          "PROTO": "up",
          "STATUS": "up"
      },
      {
          "INTF": "Loopback2",
          "IPADDR": "unassigned",
          "PROTO": "up",
          "STATUS": "up"
      }
  ]
}




■ 4. Genie Parser

4.1. Genie Parser とは

Genie Parser は、Cisco 機器に強いパーサーです。

4.2. Genie Parser 単体の利用例

Genie にはネットワーク機器への接続機能があります。testdeb と呼ばれる接続情報を YMAL で定義し、コードから呼び出します。

コード

  • testbed (接続情報)ファイル
testbed: 
  name: "testbed1"
devices:
  csr1000v:
      type: catalyst
      platform: iosxe
      os: "iosxe"
      alias: "ios1"
      tacacs:
          login_prompt: "login:"
          password_prompt: "Password:"
          username: testuser
      passwords:
          tacacs: testpass
      # enable: testpass
      # line: testpass
      connections:
          ssh:
              protocol: ssh
              ip: "10.10.20.48"
  • コード
from genie.conf import Genie
from pprint import pprint

# testbed (接続情報)を読み込む
testbed = Genie.init("./testbed.yml") 
device = testbed.devices["csr1000v"]

# 機器への接続
device.connect()

# コマンド実行、パース
output = device.parse("show ip interface brief")

# 表示
pprint(output)

実行結果

以下のようにパースされます。最初はログインや ter len 0 などのログが流れます。(device.connect(log_stdout=False) とすることで非表示することも可能。@ccieojisanさん、情報ありがとうございます)

[2019-09-04 15:30:20,523] +++ csr1000v logfile /tmp/csr1000v-cli-20190904T153020523.log +++
[2019-09-04 15:30:20,524] +++ Unicon plugin iosxe +++
Password: 
[2019-09-04 15:30:28,022] +++ connection to spawn: ssh -l developer 10.10.20.48, id: 4458345528 +++
[2019-09-04 15:30:28,023] connection to csr1000v

...(略)...

{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
                                    'ip_address': '10.10.20.48',
                                    'method': 'NVRAM',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet2': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'GigabitEthernet3': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'Loopback0': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'},
               'Loopback1': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'},
               'Loopback2': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'}}}

4.3. Genie Parser + Netmiko 利用例

Netmiko 2.4.1 で Genie Parser との連携機能が追加されました。

Netmiko から Genie Parser を利用するには、あらかじめンストールしておく必要があります。

$ pip install genie

コード

send_command の引数に、use_genie=True を付加するのがポイントです。

from netmiko import Netmiko
import os
from pprint import pprint

# 接続情報の定義
ios1 = {
  "host": "10.10.20.48",
  "device_type": "cisco_ios",
  "username": "testuser",
  "password": "testpass"
}

# 接続
net_connect = Netmiko(**ios1)

# コマンド実行、パース
result = net_connect.send_command("show ip interface brief", use_genie=True)

# 表示
pprint(result)

実行結果

以下のようにパースされます。

{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
                                    'ip_address': '10.10.20.48',
                                    'method': 'NVRAM',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet2': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'GigabitEthernet3': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'Loopback0': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'},
               'Loopback1': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'},
               'Loopback2': {'interface_is_ok': 'YES',
                             'ip_address': 'unassigned',
                             'method': 'unset',
                             'protocol': 'up',
                             'status': 'up'}}}

4.4. Genie Parser + Ansible 利用例

今度は Ansible と組み合わせた利用例です。

Ansible から Genie Parser を利用するには、clay584.parse_genie というロールが必要です。Genie Parser に加えて、本ロールもあらかじめインストールしておきます。

$ pip install geneie
$ ansible-galaxy install clay584.parse_genie

なお、本ロールは Ansible 2.7 以上が必要です。

Playbook

- hosts: ios
  gather_facts: no
  vars:  # 実行 show コマンドの定義
    show: show ip interface brief

  tasks:
    - name: Read in parse_genie role
      include_role:         # clay584.parse_genie ロールを利用
        name: clay584.parse_genie
        
     - name: ios command test
      ios_command:          # コマンド実行
        commands:
          - "{{ show }}"
      register: result      # 実行結果が変数 result に入る

     - name: parsed result  # parse_genie フィルターでパース
      debug:
        msg: "{{ result.stdout[0] | parse_genie(command=show, os='iosxe') }}"

実行結果

以下のようにパースされます。

ok: [ios1] => {
    "msg": {interface”: {
            "GigabitEthernet1": {
                "interface_is_ok": "YES",
                "ip_address": "10.10.20.48",
                "method": "NVRAM",
                "protocol": "up",
                "status": "up"
            },
            "GigabitEthernet2": {
                "interface_is_ok": "YES",
                "ip_address": "unassigned",
                "method": "NVRAM",
                "protocol": "down",
                "status": "administratively down"
            },
            "GigabitEthernet3": {
                "interface_is_ok": "YES",
                "ip_address": "unassigned",
                "method": "NVRAM",
                "protocol": "down",
                "status": "administratively down"
            },
            "Loopback0": {
                "interface_is_ok": "YES",
                "ip_address": "unassigned",
                "method": "unset",
                "protocol": "up",
                "status": "up"
            },
            "Loopback1": {
                "interface_is_ok": "YES",
                "ip_address": "unassigned",
                "method": "unset",
                "protocol": "up",
                "status": "up"
            },
            "Loopback2": {
                "interface_is_ok": "YES",
                "ip_address": "unassigned",
                "method": "unset",
                "protocol": "up",
                "status": "up"
            }
        }
    }
}


■ 5. まとめ


参考資料