LoginSignup
23
23

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-30

はじめに

以前の記事で、ポート管理表からConfigを自動生成する例をご紹介しました。
ポート管理表+パラメータ表+Jinja2テンプレートから、L2SWのConfigを自動生成してみた

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

TTP概要

ドキュメントのQuick startの簡易例をそのままご紹介すると、「(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形式)

[
    [
        {
            "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

Pythonコード

大まかな流れは以下の通りです。

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

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

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

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)ですが、説明のため、設定項目毎に3つに分割しています。

(1) グローバル設定

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

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テンプレートに似た形で、パースしたい箇所を{{ ~ }}で指定しています。
出力結果は以下の通りです。

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で、以下のテンプレートを作成しました。

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
    Descriptionの単語数が1つの場合、正規表現パターンWORDを指定します。<< To hqdist1 Gi0/1 >>のように、スペース区切りで複数単語がある場合は、PHRASEを指定すること、行の終わりまで取得できます。ケースバイケースでどちらも取り得る場合は、今回のようにORPHRASEを指定します。

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

  3. グループ関数 macro
    TTPでは、Pythonコードを使ったマクロ設定ができます。ここでは、マクロcheck_port_statusを作成し、port_statusの値がdownの場合、新たに作成したキー statusの値をxに指定します。upの場合はoとします。

  4. グループ関数 del
    開放/閉塞状態の情報はstatusだけあればいいので、port_statusは出力結果から削除しています。

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

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で、以下のテンプレートを作成しました。

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で、該当インターフェースの結果を除外しています。

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

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

前回のポート管理表と比較すると、Gi0/1とGi0/2にコマンド「switchport trunk allowed vlan 100-101」を追加したためセルE10とE11に違いはありますが、それ以外は基本的に同じものが生成されました。

最後に

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

23
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
23