APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

コーディング未経験者がPythonでCSV自動生成ツールを作りました

はじめに

先進サービス開発事業部の高橋です。
ブログ初投稿となります、よろしくお願いいたします。
今回はNEEDLEWORK-ScenarioWriterについて書きます。

NEEDLEWORK-ScenarioWriterとは

私が所属する部署はNEEDLEWORKというプロダクトを提供しています。
詳細はNEEDLEWORKの製品サイトよりご確認ください。
NEEDLEWORKを使用するために所定のCSVが必要になります。
ですがCSVをネットワーク機器のコンフィグファイルから手動で作成するのは時間がかかります。
そこで今回はコンフィグファイルから自動でCSVを生成するツールを作成しました。
現在はJuniper SSG5に対応しています。

Juniper SSG5のバックアップ

ブラウザからJuniper SSG5に接続しGUIを表示させます。
Configration→Update→Config file→Download Configuration from Deviceのsave to fileを選択します。

f:id:ko_takahashi:20191204180254p:plain
SSG_バックアップ
バックアップにてコンフィグファイルを取得後、ツールを使用します。
ツールの使用方法はNEEDLEWORK-ScenarioWriterからご確認ください。

開発で苦労したこと

3つです。

  • そもそもコーディング未経験であるということ
  • コンフィグファイルがテキスト形式であるということ
  • コードの可読性が著しく低いということ

1つずつ掘り下げていきます。

そもそもコーディング未経験であるということ

pythonの基礎どころかコーディング経験がなかったためこのツールがどうあるべきか、
そしてある処理をするためにどういった実装をするべきかを考えることに少し苦労しました。

コンフィグファイルがテキスト形式であるということ

「JSONならばどれほどよかったでしょう」
コンフィグファイルがテキスト形式(.txt)でした。
コンフィグファイルの情報をもとにCSVを生成するため、定型でないデータを扱うために煩雑な処理を行うケースもありました。
例えば以下のコード(1部抜粋)です。

value_noname_key = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone',
                    'to',  'dst_zone', 'src_ip', 'dst_ip', 'protocol', 'expect', 'log']
value_noname_key1 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone',
                     'to',  'dst_zone', 'src_ip', 'dst_ip', 'protocol', 'nat', 'src', 'expect', 'log']
value_noname_key2 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone', 'to',  'dst_zone',
                     'src_ip', 'dst_ip', 'protocol', 'nat', 'src', 'dip_id', 'dip_num', 'expect', 'log']
value_noname_key3 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone', 'to',
                     'dst_zone', 'src_ip', 'dst_ip', 'protocol', 'nat', 'src', 'ip', 'src_nat_ip', 'expect', 'log']
value_noname_key4 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone', 'to',
                     'dst_zone', 'src_ip', 'dst_ip', 'protocol', 'nat', 'dst', 'ip', 'dst_nat_ip', 'expect', 'log']
value_noname_key5 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone', 'to',  'dst_zone',
                     'src_ip', 'dst_ip', 'protocol', 'nat', 'src', 'dst', 'ip', 'dst_nat_ip', 'expect', 'log']
value_noname_key6 = ['set', 'policy', 'id', 'policy_id', 'from', 'src_zone', 'to',  'dst_zone', 'src_ip',
                     'dst_ip', 'protocol', 'nat', 'dst', 'ip', 'dst_nat_ip', 'port', 'dst_nat_port', 'expect', 'log']
policy_dict = []


def convert_list_to_dict(key, value, dictionary):
    d = {k: v for k, v in zip(
        key, value)}
    dictionary.append(d)


def append_noname_to_policy_dict(value):
    dictionary = policy_dict
    if len(value) == 13 and "log" in value or len(value) == 12 and "log" not in value:
        key = value_noname_key
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 15 and "log" in value or len(value) == 14 and "log" not in value:
        key = value_noname_key1
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 17 and "src" in value and "dip-id" in value and "log" in value or len(value) == 16 and "src" in value and "dip-id" in value and "log" not in value:
        key = value_noname_key2
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 17 and "src" in value and "log" in value or len(value) == 16 and "src" in value and "log" not in value:
        key = value_noname_key3
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 17 and "dst" in value and "log" in value or len(value) == 16 and "dst" in value and "log" not in value:
        key = value_noname_key4
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 18 and "log" in value or len(value) == 17 and "log" not in value:
        key = value_noname_key5
        convert_list_to_dict(key, value, dictionary)
    elif len(value) == 19 and "log" in value or len(value) == 18 and "log" not in value:
        key = value_noname_key6
        convert_list_to_dict(key, value, dictionary)


def absorb_config():
    with open(file_name) as f:
        for line in f:
            value = line.strip().split()
            if "manage" in line or "bypass" in line or "proxy-arp-entry" in line or "mtu" in line or "unset" in line or "sharable" in line:
                continue
            if "set policy id" in line and "name" in line and "from" in line:
                dictionary = policy_dict
                if len(value) == 20 and "log" in value or len(value) == 19 and "log" not in value:
                    key = value_name_key
                    convert_list_to_dict(key, value, dictionary)
                elif len(value) == 15 and "log" in value or len(value) == 14 and "log" not in value:
                    key = value_name_keyex
                    convert_list_to_dict(key, value, dictionary)
            elif "set policy id" in line and not "name" in line and "from" in line:
                append_noname_to_policy_dict(value)

このコードは以下の処理を行っています。

  • ツール使用時に指定したコンフィグファイルを読み取る
  • 特定の文字列が含まれた時に専用のリストに辞書型で追加する
    よりよい方法もあると思うので順次改修予定です。
    名前付きのポリシーやNATなどの設定によって「リストのn番目に必ずこの値が入る」と断定できなかったため複雑になってしまいました。

コードの可読性が著しく低いということ

これが1番苦労しました。
自分で書いたコードが2,3時間後にわからなくなる、あるいは理解に時間かかるコードを書いていました。
例えば次のコードです。

for num in range(len(DstZone)):
         if DstZone[num] == absorb.IntZoneV[0][6]:
             DstVlan += [absorb.IntZoneV[0][4]] 

このコードは「宛先ゾーンと一致するゾーンで使用されているVLANIDをリストに追加する」という処理ですが下記の情報が瞬時に読み取れるでしょうか。

  • absorb.pyの変数IntZoneVにVLANの設定をしている、Interfaceのゾーン情報が
    リストにリストで格納されている(リスト内リストである)こと
  • 実装時は格納された値のうちから固定で[0]の値を取得していたこと
  • [6]からInterfaceのゾーン名が取得できること
  • [4]からInterfaceのVLANIDが取得できること

上記のコードはまだ比較的理解しやすいですがこのようなコードや
このコード以上に複雑なコードが複数あったらどうでしょうか。
コーディングどころではなくなってしまいます。
そこで試しにradonで開発中のコードを計測してみました。

$ radon mi -s main/
main/expect.py - A (50.85)
main/description.py - A (99.93)
main/dstip.py - C (0.00)
main/srcvlan.py - A (75.34)
main/gencsv.py - A (64.28)
main/dstvlan.py - A (75.34)
main/dstfw.py - A (40.67)
main/protocol.py - A (56.32)
main/srcnatip.py - A (50.87)
main/multiple.py - C (0.00)
main/srcfw.py - A (58.35)
main/srcip.py - C (0.00)
main/srcport.py - A (81.06)
main/dstport.py - A (26.38)
main/dstnatport.py - A (62.70)
main/dstnatip.py - A (68.87)
main/absorbdict.py - A (25.38)

コードの可読性が著しく低いことがわかります。
開発過程で一部構造の変更もありましたがリーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)flake8をもとにリファクタリングしました。
リファクタリング後の結果はこのようになりました。

$ radon mi -s main/
main/expect.py - A (54.49)
main/description.py - A (97.09)
main/dstip.py - A (22.13)
main/srcvlan.py - A (81.24)
main/gencsv.py - A (62.32)
main/dstvlan.py - A (81.24)
main/dstfw.py - A (74.19)
main/protocol.py - A (58.72)
main/srcnatip.py - A (50.64)
main/multiple.py - A (44.91)
main/srcfw.py - A (78.63)
main/srcip.py - A (23.77)
main/srcport.py - A (86.96)
main/dstport.py - A (52.72)
main/dstnatport.py - A (67.09)
main/dstnatip.py - A (73.95)
main/absorbdict.py - B (17.58)

まだ改善の余地はありそうなので時間を見つけてリファクタリング等したいと思います。

以上、ご一読ありがとうございました。