はじめに
以前の記事で、ポート管理表から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コード
大まかな流れは以下の通りです。
-
パース
parse_config()
を使って、ConfigとテンプレートからJSON形式でパース結果を出力。 -
ポート管理表の生成
JSONを辞書形式に変換し、write_dict_to_csv()
でL2インターフェースのパース結果をCSV形式のポート管理表に変換。
次の項目以降で、それぞれの結果をご紹介します。
# -*- 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)で作成しているため、ここでは、それ以外のグローバル設定を定義しています。
<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テンプレートに似た形で、パースしたい箇所を{{ ~ }}
で指定しています。
出力結果は以下の通りです。
[
{
"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
で、以下のテンプレートを作成しました。
<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つの追加機能を使っています。
-
正規表現パターン ORPHRASE
Descriptionの単語数が1つの場合、正規表現パターンWORD
を指定します。<< To hqdist1 Gi0/1 >>
のように、スペース区切りで複数単語がある場合は、PHRASE
を指定すること、行の終わりまで取得できます。ケースバイケースでどちらも取り得る場合は、今回のようにORPHRASE
を指定します。 -
デフォルト値の指定
default()
shutに続くコマンドをパース対象にしているため、shutdownコマンドがあった場合は、port_status
の値がdown
(閉塞状態)になります。コマンドがない場合は、デフォルトでup
(開放状態)になります。 -
グループ関数 macro
TTPでは、Pythonコードを使ったマクロ設定ができます。ここでは、マクロcheck_port_status
を作成し、port_status
の値がdown
の場合、新たに作成したキーstatus
の値をx
に指定します。up
の場合はo
とします。 -
グループ関数 del
開放/閉塞状態の情報はstatus
だけあればいいので、port_status
は出力結果から削除しています。
出力結果は以下の通りです。
[
{
"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
で、以下のテンプレートを作成しました。
<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で、該当インターフェースの結果を除外しています。
出力結果は以下の通りです。
[
{
"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で出力した結果は以下の通りです。
前回のポート管理表と比較すると、Gi0/1とGi0/2にコマンド「switchport trunk allowed vlan 100-101」を追加したためセルE10とE11に違いはありますが、それ以外は基本的に同じものが生成されました。
最後に
これまでNW機器のパーサーとしてTextFSMやGenie Parserを使っていましたが、自分の環境に合わせたパーサーを自作したい場合、機能性やカスタマイズ性を考慮すると、TTPも有力な選択肢になると思います。
今回ご紹介したファイル一式はGitHub - portlist_generatorにアップロードしていますので、参考になれば幸いです。