L2SWのConfigからポート管理表を自動生成してみた

# はじめに

以前の記事で、ポート管理表からConfigを自動生成する例をご紹介しました。
[ポート管理表+パラメータ表+Jinja2テンプレートから、L2SWのConfigを自動生成してみた](https://qiita.com/tech_kitara/items/b6ffd9790483b08b568b)

場合によっては、作成済みのConfigから、ポート管理表を作成したいケースもあると思います。
今回は、TTP（Template Text Parser）というPythonのパーサーライブラリを使って、L2SWのConfigファイルからCSV形式のポート管理表を自動生成してみました。


# TTP概要

- [GitHub - TTP](https://github.com/dmulyalin/ttp)
- [ドキュメント](https://ttp.readthedocs.io/en/latest/Overview.html)

ドキュメントの[Quick start](https://ttp.readthedocs.io/en/latest/Quick%20start.html)の簡易例をそのままご紹介すると、「(1) Config」を「(2) テンプレート」を使ってパースし、{{ ～ }}で囲った部分の文字列が「(3) 出力結果（JSONやCSV形式）」で生成されるイメージです。

(1) Config

```
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
!
```

(2) テンプレート

```
interface {{ interface }}
 ip address {{ ip }}/{{ mask }}
 description {{ description }}
 ip vrf {{ vrf }}
```

(3) 出力結果（JSON形式）

```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"
        }
    ]
]
```

# 使用したConfig

今回は、冒頭に記載した記事とほぼ同じConfigファイルを使用しました。
詳細は以下のGitHubレポジトリを参照願います。
[Configファイル - config_hqaccess1.txt](https://github.com/Tachashi/portlist_generator/blob/master/config_hqaccess1.txt)

# Pythonコード
大まかな流れは以下の通りです。

1. パース
`parse_config()`を使って、ConfigとテンプレートからJSON形式でパース結果を出力。

2. ポート管理表の生成
JSONを辞書形式に変換し、`write_dict_to_csv()`でL2インターフェースのパース結果をCSV形式のポート管理表に変換。

次の項目以降で、それぞれの結果をご紹介します。

```python:portlist_generation.py
# -*- coding: utf-8 -*-
from ttp import ttp
import json
import csv

# 各種ファイルのパスを定義
TEMPLATE = './catalyst2960_template_ttp.txt'
PORT_LIST = './port_list_hqaccess1_ttp.csv'
CONFIG_FILENAME = './config_hqaccess1.txt'
CSV_COLUMNS = ['port_no', 'speed', 'duplex', 'mode', 'vlan', 'portfast', 'status', 'description']


def parse_config(template_file, config_filename):
    with open(config_filename, 'rt') as fc:
        data_to_parse = fc.read()

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

    # 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]
    return results


def write_dict_to_csv(port_list, csv_columns, results):
    with open(port_list, 'w', newline='') as csvfile:   # Windowsの場合、newline=''が必要
        writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
        writer.writeheader()
        for data in results:
            writer.writerow(data)
    return            


def main():
    results = parse_config(TEMPLATE, CONFIG_FILENAME)
    print(results)
    results_dict = json.loads(results)
    write_dict_to_csv(PORT_LIST, CSV_COLUMNS, results_dict[0]['l2_interfaces'])


if __name__ == "__main__":
    main()
```

# テンプレートとパース結果
実際作成したのは一つのファイル（[catalyst2960_template_ttp.txt](https://github.com/Tachashi/portlist_generator/blob/master/catalyst2960_template_ttp.txt)）ですが、説明のため、設定項目毎に3つに分割しています。

## (1) グローバル設定
インターフェース関連のテンプレートは(2)(3)で作成しているため、ここでは、それ以外のグローバル設定を定義しています。

```jinja:part1_template
<group name="global_settings">
hostname {{ hostname }}
enable secret {{ secret }}
username {{ username }} privilege 15 password {{ password }}
ip domain-name {{ hostname }}
ip default-gateway {{ default_gw }}
ntp server {{ ntp_server }}
</group>
```

設定項目は`<group></group>`でグループ分割しています。グループ名は`global_settings`としています。
これにより、パース結果をJSON形式で出力した際、`"global_settings": { ～ }`の中に結果が表示されます。
見やすさや、設定項目毎に処理を分けたい場合は便利だと思います。

またグループ内で、Jinja2テンプレートに似た形で、パースしたい箇所を`{{ ～ }}`で指定しています。
出力結果は以下の通りです。

```json:part1_output_json
[
    {
        "global_settings": {
            "default_gw": "192.168.100.150",
            "hostname": "hqaccess1",
            "ntp_server": "192.168.100.44",
            "password": "cisco",
            "secret": "test",
            "username": "test"
        }
    }
]
```

## (2) VLANインターフェース設定
グループ名`vlan_interfaces`で、以下のテンプレートを作成しました。

```jinja:part2_template
<macro>
def check_port_status(data):
    if "down" in data["port_status"]:
        data["status"] = "x"
    else:
        data["status"] = "o"
    return data
</macro>

<group name="vlan_interfaces" macro="check_port_status" del="port_status">
interface Vlan{{ vlan_num }}
 description {{ vlan_desc | ORPHRASE }}
 ip address {{ ip_address }} {{ subnet }}
 shut{{ port_status | default("up") }}
!{{ _end_ }}
</group>
```

(1)に加えて、4つの追加機能を使っています。

1. 正規表現パターン [ORPHRASE](https://ttp.readthedocs.io/en/latest/Match%20Variables/Patterns.html#orphrase)
Descriptionの単語数が1つの場合、正規表現パターン`WORD`を指定します。`<< To hqdist1 Gi0/1 >>`のように、スペース区切りで複数単語がある場合は、`PHRASE`を指定すること、行の終わりまで取得できます。ケースバイケースでどちらも取り得る場合は、今回のように`ORPHRASE`を指定します。

2. デフォルト値の指定 `default()`
shutに続くコマンドをパース対象にしているため、shutdownコマンドがあった場合は、`port_status`の値が`down`（閉塞状態）になります。コマンドがない場合は、デフォルトで`up`（開放状態）になります。

3. グループ関数 [macro](https://ttp.readthedocs.io/en/latest/Groups/Functions.html#macro)
TTPでは、Pythonコードを使ったマクロ設定ができます。ここでは、マクロ`check_port_status`を作成し、`port_status`の値が`down`の場合、新たに作成したキー `status`の値を`x`に指定します。`up`の場合は`o`とします。

4. グループ関数 [del](https://ttp.readthedocs.io/en/latest/Groups/Functions.html#del)
開放/閉塞状態の情報は`status`だけあればいいので、`port_status`は出力結果から削除しています。

出力結果は以下の通りです。

```json:part2_output_json
[
    {
        "vlan_interfaces": [
            {
                "status": "x",
                "vlan_num": "1"
            },
            {
                "ip_address": "192.168.100.47",
                "status": "o",
                "subnet": "255.255.255.0",
                "vlan_desc": "<< Server Segment >>",
                "vlan_num": "100"
            }
        ]
    }
]
```

## (3) L2インターフェース設定
最後に、グループ名`l2_interfaces`で、以下のテンプレートを作成しました。

```jinja:part3_template
<macro>
def check_port_status(data):
    if "down" in data["port_status"]:
        data["status"] = "x"
    else:
        data["status"] = "o"
    return data

def check_stp_option(data):
    if "portfast" in data["stp_option"]:
        data["portfast"] = "o"
    else:
        data["portfast"] = "x"
    return data
</macro>

<group name="l2_interfaces" exclude="ip_setting, no_ip_setting" macro="check_port_status, check_stp_option" del="port_status, stp_option">
interface {{ port_no }}
 description {{ description | ORPHRASE }}
 switchport access vlan {{ vlan | default("1") }}
 switchport trunk allowed vlan {{ vlan }}
 switchport mode {{ mode | default("access") }}
 duplex {{ duplex | default("auto") }}
 speed {{ speed | default("auto") }}
 shut{{ port_status | default("up") }}
 spanning-tree {{ stp_option | default("none") }}
 ip {{ ip_setting | ORPHRASE }}
 no ip {{ no_ip_setting | ORPHRASE }}
!{{ _end_ }}
</group>
```

基本的には(1)(2)の応用ですが、ポート管理表に出力する結果をL2インターフェースのみにするため、テンプレート`ip {{ ip_setting }}`および`no ip {{ no_ip_setting }}`を使い、これにマッチするIP設定（=L3インターフェース設定）があった場合、グループ関数 [exclude](https://ttp.readthedocs.io/en/latest/Match%20Variables/Functions.html#exclude)で、該当インターフェースの結果を除外しています。

出力結果は以下の通りです。

```json:part3_output_json
[
    {
        "l2_interfaces": [
            {
                "description": "<< To PC1 >>",
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/1",
                "portfast": "o",
                "speed": "auto",
                "status": "o",
                "vlan": "100"
            },
            {
                "description": "<< To PC2 >>",
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/2",
                "portfast": "o",
                "speed": "auto",
                "status": "o",
                "vlan": "100"
            },
            {
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/3",
                "portfast": "o",
                "speed": "auto",
                "status": "x",
                "vlan": "100"
            },
            {
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/4",
                "portfast": "o",
                "speed": "auto",
                "status": "x",
                "vlan": "100"
            },
            {
                "description": "<< To PC3 >>",
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/5",
                "portfast": "o",
                "speed": "auto",
                "status": "o",
                "vlan": "101"
            },
            {
                "description": "<< To PC4 >>",
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/6",
                "portfast": "o",
                "speed": "auto",
                "status": "o",
                "vlan": "101"
            },
            {
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/7",
                "portfast": "o",
                "speed": "auto",
                "status": "x",
                "vlan": "101"
            },
            {
                "duplex": "auto",
                "mode": "access",
                "port_no": "FastEthernet0/8",
                "portfast": "o",
                "speed": "auto",
                "status": "x",
                "vlan": "101"
            },
            {
                "description": "<< To hqdist1 Gi0/1 >>",
                "duplex": "full",
                "mode": "trunk",
                "port_no": "GigabitEthernet0/1",
                "portfast": "x",
                "speed": "1000",
                "status": "o",
                "vlan": "100-101"
            },
            {
                "description": "<< To hqdist2 Gi0/1 >>",
                "duplex": "full",
                "mode": "trunk",
                "port_no": "GigabitEthernet0/2",
                "portfast": "x",
                "speed": "1000",
                "status": "o",
                "vlan": "100-101"
            }
        ]
    }
]
```

# ポート管理表の生成
前項の出力結果(3)を辞書形式に変換し、CSVで出力した結果は以下の通りです。
![portlist_output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/b6f05195-c691-9d4a-b9fa-7a5a98ca7a09.png)

[前回のポート管理表](https://qiita.com/tech_kitara/items/b6ffd9790483b08b568b#%E3%83%9D%E3%83%BC%E3%83%88%E7%AE%A1%E7%90%86%E8%A1%A8)と比較すると、Gi0/1とGi0/2にコマンド「switchport trunk allowed vlan 100-101」を追加したためセルE10とE11に違いはありますが、それ以外は基本的に同じものが生成されました。


# 最後に
これまでNW機器のパーサーとしてTextFSMやGenie Parserを使っていましたが、自分の環境に合わせたパーサーを自作したい場合、機能性やカスタマイズ性を考慮すると、TTPも有力な選択肢になると思います。
今回ご紹介したファイル一式は[GitHub - portlist_generator](https://github.com/Tachashi/portlist_generator)にアップロードしていますので、参考になれば幸いです。