muuhei’s blog

ISPのNWエンジニア(bgp)のブログ。主に最近の学習(Ansible,batfish,CML,Python)についての雑記です。記載しているスクリプト、playbookの動作に関する責任は取れませんので、よろしくお願いします

csv読み込んでDiagramsで構成図作ってくれるスクリプト書いた

記事の本筋

pythonのdiagrams使うと構成図が作れると聞いたので、ネットワーク構成図作成にチャレンジしてみた。
チマチマ試してみたら割と良さげだったので、csv読み込んでくれる形式で作ってみた(半自動、くらい)。
構成は、「自社ルータ > パッチパネル > (×N) > Peer先」を想定。
パッチパネルは架をまたぐものと、架内のものが入り乱れている想定。
JupyterNotebookでの使用を想定。

Diagramsについて

pythonのdiagramsというライブラリを使用することで、グラフを自動で描画できる。

pip install diagrams

JupyterNotebook(Anaconda)で使用する場合は、以下も追加で実行が必要。更にPathも追加が必要。

conda install python-graphviz

pythonのライブラリとは別に、Graphvizというアプリのインストールが必要。

graphviz.org

閑話休題:↓こんな構成図を作るためのスクリプト

▼上下方向の構成図
f:id:muuhei:20211220010255p:plain ▼左右方向の構成図
f:id:muuhei:20211220010445p:plain

pythonコード_csv読み込み

import pandas as pd

base_path = "C:/Users/●●●●/Documents/Python Scripts/"
file_name = "diagrams_Test_20211219.csv"

target_table = pd.read_csv(base_path+file_name,encoding="shift-jis")
# 構成によっては1列まるまる使わないこともあるので、そいつを除外してる
target_table = target_table.dropna(axis=1,how='all')
target_table

CSVファイル

circuit,local_hostname,local_logical_if,local_physical_if,peer,peer_logical_if,peer_physical_if,1st_patch,1st_Port,2nd_patch,2nd_Port,3rd_patch,3rd_Port
1,router1test,bundle-ether1,hu0/0/0/1,peer_hoge,test_001,,架間_001-011,Port1,架内_011#01,Port1,,
2,router1test,bundle-ether1,hu0/0/0/2,peer_hoge,test_001,,架間_001-011,Port3,架内_011#01,Port3,,
3,router1test,bundle-ether1,hu0/0/0/3,peer_hoge,test_001,,架間_001-011,Port5,架内_011#01,Port5,,
4,router1test,bundle-ether1,hu0/0/0/4,peer_hoge,test_001,,架間_001-011,Port7,架内_011#01,Port7,,
5,router2test,bundle-ether1,hu0/0/0/1,peer_hoge,test_002,,架間_002-012,Port1,架間_011-012,Port1,架内_011#01,Port2
6,router2test,bundle-ether1,hu0/0/0/2,peer_hoge,test_002,,架間_002-012,Port3,架間_011-012,Port3,架内_011#01,Port4
7,router2test,bundle-ether1,hu0/0/0/3,peer_hoge,test_002,,架間_002-012,Port5,架間_011-012,Port5,架内_011#01,Port6
8,router2test,bundle-ether1,hu0/0/0/4,peer_hoge,test_002,,架間_002-012,Port7,架間_011-012,Port7,架内_011#01,Port8

pythonコード_diagramsで使うアトリビュートを指定

# グラフ描画のアトリビュート
graph_attributes = {
    "pad":"1.0",
    "nodesep":"0.1",
    "ranksep":"1.50",
    "splines":"polyline",
    "fontsize":"40",
#     "bgcolor":"transparent",
}

# ノード描画に関するアトリビュート
node_attributes = {
    "nodesep": "0.1",
    "fixedsize": "True",
    "labelloc": "b",
    "margin": "0.1",
}

# ルーター描画に関するアトリビュート
graph_attributes_router = {
    "bgcolor":"#7FFFD4",
    "fontsize":"15",
    "fontcolor":"red"
}

# パッチパネル描画に関するアトリビュート
graph_attributes_patch = {
    "fontsize":"15",
    "margin":"10",
}

# Peer描画に関するアトリビュート
graph_attributes_peer = {
    "bgcolor":"#e7ff0f",
    "fontsize":"15",
    "margin":"1",
}

この辺はお好みで。
graph_attributesで使用している「"splines": "polyline"」は使った方が良い。(未使用だと curvedが何故か2本矢印になる)

python_コード:構成図を描画時に使用するフロー情報を作成。

import re
import numpy as np

# 各クラスタの構成要素を定義
host_cluster = target_table.columns[target_table.columns.str.contains("local")]
peer_cluster = target_table.columns[target_table.columns.str.contains("peer")]
patch_panel_cluster = target_table.columns[target_table.columns.str.contains("patch")]
patch_port_cluster = target_table.columns[target_table.columns.str.contains("Port")]

circuits = []
for index,circuit in target_table.iterrows():
    # 自分のスタート情報を作成
    temp_list = []
    if circuit[host_cluster[2]] is np.nan:
        if circuit[host_cluster[1]] is np.nan:
            temp_list.append(circuit[host_cluster[0]]+"_"+circuit[host_cluster[0]].replace("/","_").replace("-",""))
        else:
            temp_list.append(circuit[host_cluster[0]]+"_"+circuit[host_cluster[1]].replace("/","_").replace("-",""))
    else:
        temp_list.append(circuit[host_cluster[0]]+"_"+circuit[host_cluster[2]].replace("/","_").replace("-",""))
    
    # 途中経路情報を作成
    for patch_layer,port_layer in zip(patch_panel_cluster,patch_port_cluster):
        if circuit[patch_layer] is np.nan:
            continue
        elif "架間" in circuit[patch_layer]:
            temp_list.append(circuit[patch_layer].replace("架間","Gakan").replace("-","to")+"_"+re.findall('架間_(.*)-',circuit[patch_layer])[0]+"_"+circuit[port_layer])
            temp_list.append(circuit[patch_layer].replace("架間","Gakan").replace("-","to")+"_"+re.findall('-(.*)',circuit[patch_layer])[0]+"_"+circuit[port_layer])
        else:
            temp_list.append(circuit[patch_layer].replace("架内","Ganai").replace("#","_")+"_"+circuit[port_layer])
            
    # Peer情報を作成
    if len(peer_cluster) == 3:
        if circuit[peer_cluster[2]] is np.nan:
            temp_list.append(circuit[peer_cluster[1]])
        else:
            temp_list.append(circuit[peer_cluster[1]]+"_"+circuit[peer_cluster[2]].replace("/","_"))
    elif len(peer_cluster) == 2:
        temp_list.append(circuit[peer_cluster[0]]+"_"+circuit[peer_cluster[1]].replace("/","_"))
    else:
        temp_list.append(circuit[peer_cluster[0]])
    print(temp_list)
    circuits.append(temp_list)
# circuits

python_コード:構成図を描画時に使用するフロー情報を作成。(したものを表示)

for each_circuit in circuits:
    print(' >> '.join(each_circuit))

表示されたものをコピーしておく

python_コード:構成図を描画する

from diagrams import Diagram, Cluster, Edge
from diagrams.aws.network import VPCRouter,ELB
from diagrams.ibm.network import Bridge


Diagram_name = file_name.replace(".csv","")

# 自分とPeerのリストを作成しよう
# 自分
local_host_list = list(sorted(set(target_table[host_cluster[0]])))
# Peer
peer_list = list(sorted(set(target_table[peer_cluster[0]])))

with Diagram(Diagram_name, 
             show=False,
             direction="TB", # "TB"は「上⇒下」方向での構成図描画(TopBottom?)。ここをコメントアウトすると、「左⇒右」の構成図になる。
             curvestyle = "curved",
             graph_attr=graph_attributes, 
             node_attr=node_attributes) as hogege:
    
    # 弊社のクラスタにしよう
    with Cluster("弊社"):
        
        # 自分たちのルータがノードかクラスタかを判定
        for each_router in local_host_list:
            if len(target_table.loc[target_table[host_cluster[0]] == each_router].dropna()) == 1:
                exec(str(each_router)+" = VPCRouter("+str(each_router)+",graph_attr=graph_attributes_router)")
            else:
                with Cluster(each_router,graph_attr=graph_attributes_router):
                    for each_logical_if in list(sorted(set(target_table.loc[target_table[host_cluster[0]] == each_router][host_cluster[1]]))):
                    # もし物理IFが無い場合は、論理IFをノードにするための判定式
                        if len(target_table.loc[(target_table[host_cluster[0]] == each_router) & (target_table[host_cluster[1]] == each_logical_if)][host_cluster[2]].dropna()) == 0:
                            exec(str(each_router+"_"+each_logical_if.replace("/","_"))+" = VPCRouter(each_logical_if)")
                        else:
                            with Cluster(each_logical_if):
                                for each_physical_if in list(sorted(set(target_table.loc[target_table[host_cluster[0]] == each_router][host_cluster[2]]))):
                                    exec(str(each_router+"_"+each_physical_if.replace("/","_"))+" = VPCRouter(each_physical_if)")
    
#     # Peer先がノードかクラスタかを判定するぞ
    for each_router in peer_list:
        if len(target_table.loc[target_table[peer_cluster[0]] == each_router].dropna()) == 1:
            exec(str(each_router)+" = VPCRouter("+str(each_router)+",graph_attr=graph_attributes_peer)")
        else:
            with Cluster(each_router,graph_attr=graph_attributes_peer):
                for each_logical_if in list(sorted(set(target_table.loc[target_table[peer_cluster[0]] == each_router][peer_cluster[1]]))):
                    # もし物理IFが無い場合は、論理IFをノードにするための判定式
                    if len(peer_cluster) == 3:
                        if len(target_table.loc[(target_table[peer_cluster[0]] == each_router) & (target_table[peer_cluster[1]] == each_logical_if)].dropna()) == 0:
                            exec(str(each_router+"_"+each_logical_if.replace("/","_"))+" = VPCRouter(each_logical_if)")
                    elif len(peer_cluster) <= 2:
                        exec(str(each_router+"_"+each_logical_if.replace("/","_"))+" = VPCRouter(each_logical_if)")
                    else:
                        with Cluster(each_logical_if):
                            for each_physical_if in list(sorted(set(target_table.loc[target_table[peer_cluster[0]] == each_router].dropna()))):
                                exec(str(each_router+"_"+each_physical_if.replace("/","_"))+" = VPCRouter(each_physical_if)")
    
    # 途中経路(パッチパネル)のクラスタをゴリゴリ作るぞ
    for patch_layer,port_layer in zip(patch_panel_cluster,patch_port_cluster):
        for each_patch in list(sorted(set(target_table[patch_layer].dropna()))):
            if "架間" in each_patch:
                with Cluster(each_patch, graph_attr=graph_attributes_patch):
                    # 架間の片側のクラスタ作成
                    with Cluster(each_patch+"("+re.findall('架間_(.*)-',each_patch)[0]+")", graph_attr=graph_attributes_patch):
                        for each_port in list(sorted(set(target_table.loc[target_table[patch_layer] == each_patch][port_layer].dropna()))):
                            exec(str(each_patch.replace("架間","Gakan").replace("-","to"))+"_"+re.findall('架間_(.*)-',each_patch)[0]+"_"+str(each_port)+" = Bridge(each_port)")
                        
                    # 架間の逆側のクラスタ作成
                    with Cluster(each_patch+"("+re.findall('-(.*)',each_patch)[0]+")", graph_attr=graph_attributes_patch):
                        for each_port in list(sorted(set(target_table.loc[target_table[patch_layer] == each_patch][port_layer].dropna()))):
                            exec(str(each_patch.replace("架間","Gakan").replace("-","to"))+"_"+re.findall('-(.*)',each_patch)[0]+"_"+str(each_port)+" = Bridge(each_port)")
                    
            elif "架内" in each_patch:
                with Cluster(each_patch, graph_attr=graph_attributes_patch):
                    for each_port in list(sorted(set(target_table.loc[target_table[patch_layer] == each_patch][port_layer].dropna()))):
                        exec(str(each_patch.replace("架内","Ganai").replace("#","_"))+"_"+str(each_port)+" = Bridge(each_port)")
        

    # 経路を追加する(1個前のセクションのやつをコピペするのが吉。本当はループでうまく貼りたかったけどできなかった)
    router1test_hu0_0_0_1 >> Gakan_001to011_001_Port1 >> Gakan_001to011_011_Port1 >> Ganai_011_01_Port1 >> peer_hoge_test_001
    router1test_hu0_0_0_2 >> Gakan_001to011_001_Port3 >> Gakan_001to011_011_Port3 >> Ganai_011_01_Port3 >> peer_hoge_test_001
    router1test_hu0_0_0_3 >> Gakan_001to011_001_Port5 >> Gakan_001to011_011_Port5 >> Ganai_011_01_Port5 >> peer_hoge_test_001
    router1test_hu0_0_0_4 >> Gakan_001to011_001_Port7 >> Gakan_001to011_011_Port7 >> Ganai_011_01_Port7 >> peer_hoge_test_001
    router2test_hu0_0_0_1 >> Gakan_002to012_002_Port1 >> Gakan_002to012_012_Port1 >> Gakan_011to012_011_Port1 >> Gakan_011to012_012_Port1 >> Ganai_011_01_Port2 >> peer_hoge_test_002
    router2test_hu0_0_0_2 >> Gakan_002to012_002_Port3 >> Gakan_002to012_012_Port3 >> Gakan_011to012_011_Port3 >> Gakan_011to012_012_Port3 >> Ganai_011_01_Port4 >> peer_hoge_test_002
    router2test_hu0_0_0_3 >> Gakan_002to012_002_Port5 >> Gakan_002to012_012_Port5 >> Gakan_011to012_011_Port5 >> Gakan_011to012_012_Port5 >> Ganai_011_01_Port6 >> peer_hoge_test_002
    router2test_hu0_0_0_4 >> Gakan_002to012_002_Port7 >> Gakan_002to012_012_Port7 >> Gakan_011to012_011_Port7 >> Gakan_011to012_012_Port7 >> Ganai_011_01_Port8 >> peer_hoge_test_002
    
hogege

これを実行すると、冒頭後半の構成図ができる。ついでにNotebook保存ディレクトリに、画像ファイルも生成される。

Diagramについて(感想)

トラディショナルな企業での管理構成図にするのは難しそうだけど、作業手順書に添付する形式ならイケそう。
主にクラウド方面が強い、というかそっち方面しかカバーしてなさそう。
本職NWエンジニアとしてはアイコンのバリエーションがちと寂しい。
でもアイコンも追加することができるっぽい?
それより、矢印がきちんと結線しないのが気になるけど、メインの業界だと気にならないもの?

以上

CML(CiscoModelingLabs)を2.0から2.2にアップデートしたらCockpitにアクセスできなくなった件

事象について

ほぼほぼタイトル通りだけど、CMLを2.0からアップデートしたらCockpit(http://[address]:9090/)にアクセスできなくなった。
Access Denied
ラボマネージャ自体にはアクセスできたし、ラボ内の機器も起動/操作できたので大枠問題なかったけど、
ファイアウォール設定が弄れないと色々不都合あったので対処。

結論としては既知のBugで、公式に掲載されているWorkaround実施で解消。
でもこの公式情報になかなかリーチできなかったので備忘として。

具体的な対処

certファイルを削除して、cockpitをrestartするだけ。

sudo rm /etc/cockpit/ws-certs.d/0-self-signed.cert
sudo systemctl restart cockpit

https://developer.cisco.com/docs/modeling-labs/#!cml-release-notes/known-issues-and-caveats-for-cml-22

After the services restart or after a software update, Cockpit fails to start. If you navigate with your web browser to your CML server, port 9090, you will get a connection refused error.

[参考]ファイアウォール設定弄れないと???

おま環かもしれないので参考で。

何故か「ネットワーキング > ファイアウォール:オン」でないと、
CMLラボ内からお外にアクセスできない。

また、「ネットワーキング > ファイアウォール:オフ」でないと、
自身のホストPCからCMLラボ内の機器にアクセスできない。(これは想定挙動と思われる)
[↑ができないと困る理由)
 ・ホストPC上のtrexアプリから、CMLラボ内のtrexにアクセスできない
 ・ホストPC上のtwsnmpからsnmpgetでトラヒックグラフ表示したりできない

以上

CML(Cisco Modeling Labs)-Personalを使って色々遊ぶ

CMLについて

CMLCiscoが提供している仮想的なネットワークシミュレーションプラットフォーム。 ライセンス形態が複数ある。本Blogでは、断りが無い限りは基本的にCML-Personalについて記載。 ライセンス形態や初期構築手順の詳細については、↓以下を参照   learningnetwork.cisco.com

CML内に構築したネットワークトポロジ(GoogleChrome

f:id:muuhei:20201011160023p:plain
ブラウザ上の管理画面

CMLの良いところ

以下により、多種多様な検証を行う箱庭として非常に優れている

  • ネットワーク機器を仮想的に動作させることができること
  • そのネットワーク機器に、Teratermを使用してアクセス可能なこと
  • ネットワーク機器以外にもサーバ(ubuntu)を構築でき、そのサーバ上でAnsibleやSyslog,SNMP,Python等を動作させられるところ
  • 構築したノード間のパケットキャプチャが可能なこと
  • TRexを別途インストールすることで、トラフィック生成もシミュレートできること
  • Windows上で構築した場合、twsnmpを使ってトラフィック遷移を可視化できること
  • External Connectorを使用して、CML内部の機器が外部NWにもアクセス可能なこと

CML使用の注意点

  • 「Wipe Labs」というボタンをクリックすると、ラボ内の機器の設定が消えること
  • TRex使用時、CML内でTRexを起動した後に「Ctrl + B」→「c」と入力しないと、CML画面に遷移しないこと
  • TRexで使用するポートが、CMLファイアウォールで許可対象ポートに含まれていないこと
  • メモリをガシガシ使うこと。IOSXRやNX-OSを使用する場合は16GBでも心細い

こういうことに活用しよう

以上