Ansibleのテキストパース用フィルタープラグインを自作してみた

# はじめに

以前の記事で、TTP（Template Text Parser）というPythonのパーサーライブラリを使って、L2SWのConfigファイルをパースし、ポート管理表の自動生成を行いました。
[L2SWのConfigからポート管理表を自動生成してみた](https://qiita.com/tech_kitara/items/75ec526ff9a27932c565)

今回は、このパーサーをAnsibleのカスタムフィルターとして取り込み、`xxx_command`モジュールで取得したshowコマンド結果をパースできるようにしてみました。

※ フィルタープラグイン自作例は、以前[こちら](https://qiita.com/tech_kitara/items/9bcf2ca6900ef76f6324)でも紹介しています。
※ 私が知る限り、パース用フィルタープラグインとして、他にもTextFSMを利用した`parse_cli_textfsm`や、pyATS/Genieを利用した`parse_genie`（Ansible Galaxyからインストールが必要。詳細は[GitHub/parse_genie](https://github.com/clay584/parse_genie)に記載。）があります。


# セットアップ

Python3.6.7の仮想環境内にインストールしたAnsibleを使いました。当初、バージョン2.9.0でテストしていたのですが、Logging error（KeyError）メッセージが複数出たため、2.8.4を使っています。

追加でTTPのインストールが必要です。

```
(venv) [centos@localhost ansible]$ pip install ttp
```

# カスタムフィルター

以下のPythonスクリプトを作成しました。

```python:custom_filters_ttp.py
from ansible.errors import AnsibleError

# ttp、jsonのインポート。インポートに失敗した場合、後続の処理でエラー出力できるようにする。
try:
    from ttp import ttp
    HAS_TTP = True
except ImportError:
    HAS_TTP = False

try:
    import json
    HAS_JSON = True
except ImportError:
    HAS_JSON = False


class FilterModule(object):

    def parse_cli_ttp(self, cli_output, template_file):
        if not HAS_TTP:
            raise AnsibleError('parse_cli_ttp filter requires TTP library to be installed')

        if not HAS_JSON:
            raise AnsibleError('parse_cli_ttp filter requires JSON library to be installed')

        with open(template_file, 'rt') as ft:
            ttp_template = ft.read()

        # create parser object and parse data using template
        parser = ttp(data=cli_output, template=ttp_template)
        parser.parse()

        # return result in JSON format
        results = parser.result(format='json')[0]
        return results

    def filters(self):
        return {
            # 左側がPlaybook内で使用するフィルター名、右側が紐付ける関数名。
            'parse_cli_ttp': self.parse_cli_ttp,
        }
```

ベストプラクティスに従い、Playbookを格納しているディレクトリ配下に、filter_pluginsディレクトリを作成し、その中に本ファイルを格納しました。

また、Ansibleがこのカスタムフィルターを認識できるよう、ansible.cfgの設定を以下の通り書き換えました。

```ansible.cfg
[defaults]
filter_plugins     = [Playbook格納ディレクトリのフルパス]/filter_plugins
```

# Playbook
`ios_command`モジュールでRunning Configを取得し、続く`debug`モジュール内で、取得したConfigと以前の記事で作成した[L2インターフェース設定用テンプレート](https://qiita.com/tech_kitara/items/75ec526ff9a27932c565#3-l2%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E8%A8%AD%E5%AE%9A)のファイルパスを指定しました。シンプルですね！

```yaml:playbook_ttp.yml
---

- hosts: cisco
  gather_facts: no
  connection: network_cli

  tasks:
    - name: run show command on remote devices
      ios_command:
        commands: show running-config
      register: result

    - name: display parsed output
      debug:
        msg: "{{ result.stdout[0] | parse_cli_ttp('catalyst2960_template_ttp2.txt') }}"
```


# 出力結果
前回とConfigが異なるため結果に違いはありますが、パース自体は問題なくできています。

```
$ ansible-playbook -i inventory_2960.ini playbook_ttp.yml

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

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

TASK [display parsed output] *********************************************************************************
ok: [hqdist1A] => {
    "msg": [
        {
            "l2_interfaces": [
                {
                    "description": "<< Connect hqdist1 and hqdist2 >>",
                    "duplex": "auto",
                    "mode": "trunk",
                    "port_no": "Port-channel1",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1,101"
                },
                {
                    "description": "<< To hqborder1 Fa1 >>",
                    "duplex": "full",
                    "mode": "access",
                    "port_no": "FastEthernet0/1",
                    "portfast": "x",
                    "speed": "100",
                    "status": "o",
                    "vlan": "200"
                },
                {
                    "description": "<< To hqborder2 Fa1 >>",
                    "duplex": "full",
                    "mode": "access",
                    "port_no": "FastEthernet0/2",
                    "portfast": "x",
                    "speed": "100",
                    "status": "o",
                    "vlan": "202"
                },
                {
                    "description": "<< To hqaccess1 Fa0/23 >>",
                    "duplex": "full",
                    "mode": "access",
                    "port_no": "FastEthernet0/3",
                    "portfast": "x",
                    "speed": "100",
                    "status": "o",
                    "vlan": "100"
                },
                {
                    "duplex": "auto",
                    "mode": "trunk",
                    "port_no": "FastEthernet0/4",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/5",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/6",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/7",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/8",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/9",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/10",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/11",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/12",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "203"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/13",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "100"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/14",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/15",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/16",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/17",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/18",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/19",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/20",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/21",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "FastEthernet0/22",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "x",
                    "vlan": "1"
                },
                {
                    "description": "<< To hqdist2 Fa0/23 >>",
                    "duplex": "auto",
                    "mode": "trunk",
                    "port_no": "FastEthernet0/23",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1,101"
                },
                {
                    "description": "<< To hqdist2 Fa0/24 >>",
                    "duplex": "auto",
                    "mode": "trunk",
                    "port_no": "FastEthernet0/24",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1,101"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "GigabitEthernet0/1",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1"
                },
                {
                    "duplex": "auto",
                    "mode": "access",
                    "port_no": "GigabitEthernet0/2",
                    "portfast": "x",
                    "speed": "auto",
                    "status": "o",
                    "vlan": "1"
                }
            ]
        }
    ]
}

PLAY RECAP ***************************************************************************************************
hqdist1A                   : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
```

# 最後に
今回作ったプラグインは[GitHub/ansible-ttp](https://github.com/Tachashi/ansible-ttp)にアップしています。Configだけでなく、他のshowコマンドの出力結果も比較的簡単にパース出来ますので、ぜひ使ってみて頂ければと思います。