うさラボ

お勉強と備忘録

TTP(Text Template Paser)入門

やりたかったこと

前回、ntc-templateの自作に入門したので今回は同じ内容を別のパーサーで試してみます。
やることはshow ip nat translationsのパースです。

公式ドキュメント ttp.readthedocs.io

Github github.com

参考にさせていただいたブログ zaki-hmkc.hatenablog.com

準備

環境

python
* Python 3.9.6

ライブラリ
* ansible-core==2.12.0 * ttp==0.8.1

コレクション
* ansible.netcommon 2.4.0
* ansible.utils 2.4.2

実装

まずは公式ドキュメントのQuick startにあるスクリプト動かして確認を使い方を確認をします。

from ttp import ttp

data_to_parse = """
interface Loopback0
 description Router-id-loopback
 ip address 192.168.0.113/24
!
interface Vlan778
 description CPE_Acces_Vlan
 ip address 2002::fd37/124
 ip vrf CPE1
!
"""

ttp_template = """
interface {{ interface }}
 ip address {{ ip }}/{{ mask }}
 description {{ description }}
 ip vrf {{ vrf }}
"""

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

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

# or in csv format
csv_results = parser.result(format='csv')[0]
print(csv_results)

パース前のデータをdata_to_parseに格納、ttp_templateでパースをします。
result表示の際にformatの指定もできるようです。

実行してみます。

ttp# python sample.py 
# format JSON
[
    [
        {
            "description": "Router-id-loopback",
            "interface": "Loopback0",
            "ip": "192.168.0.113",
            "mask": "24"
        },
        {
            "description": "CPE_Acces_Vlan",
            "interface": "Vlan778",
            "ip": "2002::fd37",
            "mask": "124",
            "vrf": "CPE1"
        }
    ]
]

# format CSV
"description","interface","ip","mask","vrf"
"Router-id-loopback","Loopback0","192.168.0.113","24",""
"CPE_Acces_Vlan","Vlan778","2002::fd37","124","CPE1"

JSONCSVになったデータが表示されました。

テンプレート部分に注目をすると使い方のイメージがつくと思います。 出力結果を抜き出して変数として登録するようなテンプレートを作成します。

# Templateファイル
interface {{ interface }}
 ip address {{ ip }}/{{ mask }}
 description {{ description }}
 ip vrf {{ vrf }}

# パース前
interface Vlan778
 description CPE_Acces_Vlan
 ip address 2002::fd37/124
 ip vrf CPE1

# パース後
{
    "description": "CPE_Acces_Vlan",
    "interface": "Vlan778",
    "ip": "2002::fd37",
    "mask": "124",
    "vrf": "CPE1"
}

ある程度テンプレートも用意されているようです。 github.com

それでは他のテンプレートを参考にshow ip nat translationsのパーサーを作成します。 show ip nat translationsの結果は以下です。

Pro Inside global      Inside local       Outside local      Outside global
tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21
tcp 10.9.0.0:51778     10.1.0.2:51778     10.2.0.2:21        10.2.0.2:21
tcp 10.9.0.0:56384     10.1.0.2:56384     10.2.0.2:22        10.2.0.2:22
icmp 10.9.0.0:56111     10.1.0.2:56384     10.2.0.2:23        10.2.0.2:23
--- 10.9.0.0     10.1.0.2     ---        ---
--- 10.9.0.1     10.1.0.3     ---        ---

Pro以外の要素はipip:portのルールで表記されています。
IPのみで表記されているのがstatic natになる想定なので、テンプレートでマッチする条件を調整します。

# dynamic natのエントリー
tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21

# static natのエントリー
--- 10.9.0.0     10.1.0.2     ---        ---

作成したテンプレートが以下です。

<group name="dynamic_nat">
{{ protocol }} {{ inside_global_ip }}:{{ inside_global_port }} {{ inside_local_ip }}:{{ inside_local_port }} {{ outside_local_ip }}:{{ outside_local_port }} {{ outside_global_ip }}:{{ outside_global_port }}
</group>

<group name="static_nat">
{{ protocol }} {{ inside_global_ip | re("(\S+)(?!.*:)") }} {{ inside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_global_ip | re("(\S+)(?!.*:)") }}
</group>

でくくることでdynamicとstaticを別のグループにしています。 static_natのグループでは:が表示結果に含まれていないことを条件にしました。

今回はanisbleでパースをします、templatesディレクトリにテンプレートファイルを配置します。

.
├── sample.yml
└── templates
    └── ios_show_ip_nat_translations.ttp <--作成したパーサー

ansible-playbookを作成します。 ansible.utiles.cli_parseを利用します。 tempalte_pathはなぜかtemplates配下を見に行ってくれなかったのでplaybookのある階層からtemplateファイルを指定しています。

---
- name: Test Template Text Parser
  hosts: localhost
  gather_facts: false

  tasks:
    - name: parse
      ansible.utils.cli_parse:
        text: |-
          Pro Inside global      Inside local       Outside local      Outside global
          tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21
          tcp 10.9.0.0:51778     10.1.0.2:51778     10.2.0.2:21        10.2.0.2:21
          tcp 10.9.0.0:56384     10.1.0.2:56384     10.2.0.2:22        10.2.0.2:22
          icmp 10.9.0.0:56111     10.1.0.2:56384     10.2.0.2:23        10.2.0.2:23
          --- 10.9.0.0     10.1.0.2     ---        ---
          --- 10.9.0.1     10.1.0.3     ---        ---
        parser:
          name: ansible.netcommon.ttp
          template_path: ./templates/ios_show_ip_nat_translations.ttp
      register: res_parsed

    - name: debug
      debug:
        msg: "{{ res_parsed.parsed[0][0] }}"

確認

ansible-playbookを実行して動作を確認します。

PLAY [Test Template Text Parser] *****************************************************************************************************************************

TASK [parse] *************************************************************************************************************************************************
[WARNING]: Use 'ansible.utils.ttp' for parser name instead of 'ansible.netcommon.ttp'. This feature will be removed from 'ansible.netcommon' collection in a
release after 2022-11-01
ok: [localhost]

TASK [debug] *************************************************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "dynamic_nat": [
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "51776",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "51776",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "21",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "21",
                "protocol": "tcp"
            },
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "51778",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "51778",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "21",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "21",
                "protocol": "tcp"
            },
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "56384",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "56384",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "22",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "22",
                "protocol": "tcp"
            },
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "56111",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "56384",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "23",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "23",
                "protocol": "icmp"
            }
        ],
        "static_nat": [
            {
                "inside_global_ip": "10.9.0.0",
                "inside_local_ip": "10.1.0.2",
                "outside_global_ip": "---",
                "outside_local_ip": "---",
                "protocol": "---"
            },
            {
                "inside_global_ip": "10.9.0.1",
                "inside_local_ip": "10.1.0.3",
                "outside_global_ip": "---",
                "outside_local_ip": "---",
                "protocol": "---"
            }
        ]
    }
}

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

パースされ、想定通りの結果を得ることができました。 dynamic_natグループはポートの情報も分けて取得することができています。

パース後の要素が2個以上ある場合は自動的にリストになるようです。要素が1個の場合リストにならないため、パース後に呼び出す際に注意が必要です。
テンプレートの書き方を調整すれば常にリストになるようにできるかもしれません。(※)

(※) 2021/12/16更新 解決方法が分かりました usage-automate.hatenablog.com

パース後の結果がけっこう深い階層にあったのがよくわからなかったですが、動作としては問題なかったです。res_parsed.parsed[0][0]

まとめ

TTP入門してみました。テンプレートがとても簡単に作れることができ、かなり便利です。
テンプレート自作ができるようになるとCLIの機器自動化でできることがかなり増えます、よければ参考にしてください。