🎨

NW図 as Code(3) Graphvizを試す

2022/06/15に公開

Graphviz

Graphvizというツールを使って物理ネットワーク図を描いてみました。
Graphvizはテキスト形式で記述したデータを図形に変換してくれるツールです。
Graphという文字列が含まれている通り元々はグラフ[1]を描くためのツールです。
これをネットワーク図作成に利用してみます。

https://graphviz.org/

結果

まず最初に、できた図を貼っておきます。

この図の書き方を以下で説明していきます。

利用方法

Ubuntuにはgraphvizというパッケージがあるので、これをインストールしてもいいんのですが、ちょっと試すだけならオンラインGraphvizエディターを公開している方もいるようなので、そちらを使わせてもらうのが楽そうです。

今回はこちらをお借りしました。

https://dreampuf.github.io/GraphvizOnline/

オブジェクトと線の定義

先ほどのオンラインエディターを開いて、左ペインに以下のテキストをコピペしてみましょう。

graph physical_net_topology {
    A [shape=box]
    B [shape=box]
    C [shape=box]
    D
    A -- B
    A -- C
    C -- D
}

四角形と線が作れました。先ほど述べた通り、Graphvizはグラフを描くためのツールなので、Graphvizでは「ノード」、「エッジ」と呼びます。
A、B、Cはshape=boxと書くことで四角形になります。何も書かないとDのように楕円のオブジェクトが描画されます。

グループ化

今回はネットワーク機器とネットワークインターフェイスを図示したいので、四角形の入れ子を描きたいです。

Graphvizではsubgraphという機能を使います。

graph physical_net_topology {
    subgraph cluster_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
    }
    subgraph cluster_router1 {
        label = router1
        router1_e0 [shape=box, label="e0"]
        router1_e2 [shape=box, label="e2"]
    }
    router0_e0 -- router1_e0
}

  • router0_e0という(Graphviz用語でいうところの)ノードを作成
    • このノードのラベル(図示するときの文字列)は「e0」
  • router0_e0, router0_e1をsubgraphに入れることでグループ化
    • このグループのラベルは「router0」
    • cluster_router0がこのsubgraphの名前
    • Graphvizの仕様上、subgraphの名前が「cluster」から始まるときはこのsubgraphは線で囲まれます

最後の「名前がclusterから始まるとき」というのが要注意で、以下のように違う名前にしたり、スペルミスをすると回りの線がなくなります。

graph physical_net_topology {
    subgraph subgraph_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
    }
    subgraph cruster_router1 {
        label = router1
        router1_e0 [shape=box, label="e0"]
        router1_e2 [shape=box, label="e2"]
    }
    router0_e0 -- router1_e0
}

同種の装置を同じ段に並べる

routerとl3switchを書いてみました。

graph physical_net_topology {
    subgraph cluster_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
    }
    subgraph cluster_router1 {
        label = router1
        router1_e0 [shape=box, label="e0"]
        router1_e2 [shape=box, label="e2"]
    }
    subgraph cluster_l3sw0 {
        label = l3sw0
        l3sw0_e0 [shape=box, label="e0"]
        l3sw0_e1 [shape=box, label="e1"]
        l3sw0_e2 [shape=box, label="e2"]
        l3sw0_e3 [shape=box, label="e3"]
        l3sw0_e4 [shape=box, label="e4"]
        l3sw0_e5 [shape=box, label="e5"]
    }
    subgraph cluster_l3sw1 {
        label = l3sw1
        l3sw1_e0 [shape=box, label="e0"]
        l3sw1_e1 [shape=box, label="e1"]
        l3sw1_e2 [shape=box, label="e2"]
        l3sw1_e3 [shape=box, label="e3"]
        l3sw1_e4 [shape=box, label="e4"]
        l3sw1_e5 [shape=box, label="e5"]
    }

    router0_e0 -- router1_e0
    router0_e2 -- l3sw0_e2
    router1_e2 -- l3sw1_e2
}

router0が最上段、2段目がrouter1とl3sw0、3段目にl3sw1になりました。
Graphvizはグラフを描くツールというのがここでも効いています。最初に定義したrouter0(のe0とe2)がトップレベルのノードになり、そこからエッジ1本で接続されているrouter1_e0とl3sw0_e2が次のレベルに来る形になっており、グラフとしてはきれいな形なんですよね。

ただ、ネットワーク図を描くのなら、router0とrouter1は冗長化されたペアであり同列、という形にしたいです。

graph physical_net_topology {
    newrank=true
    subgraph cluster_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
        rank=same
    }
    subgraph cluster_router1 {
        label = router1
        router1_e0 [shape=box, label="e0"]
        router1_e2 [shape=box, label="e2"]
        rank=same
    }
    subgraph cluster_l3sw0 {
        label = l3sw0
        l3sw0_e0 [shape=box, label="e0"]
        l3sw0_e1 [shape=box, label="e1"]
        l3sw0_e2 [shape=box, label="e2"]
        l3sw0_e3 [shape=box, label="e3"]
        l3sw0_e4 [shape=box, label="e4"]
        l3sw0_e5 [shape=box, label="e5"]
        rank=same
    }
    subgraph cluster_l3sw1 {
        label = l3sw1
        l3sw1_e0 [shape=box, label="e0"]
        l3sw1_e1 [shape=box, label="e1"]
        l3sw1_e2 [shape=box, label="e2"]
        l3sw1_e3 [shape=box, label="e3"]
        l3sw1_e4 [shape=box, label="e4"]
        l3sw1_e5 [shape=box, label="e5"]
        rank=same
    }
    {rank=same; router0_e0; router1_e0}
    {rank=same; l3sw0_e0; l3sw1_e0}
    router0_e0 -- router1_e0
    router0_e2 -- l3sw0_e2
    router1_e2 -- l3sw1_e2
}

rankという機能を使います。ランキングのrankです。この機能でノードのrank(順位)を決めてあげます。
Graphvizのrankには古い計算方法(newrank=false)と新しい計算方法(newrank=true)があります。clusterの中のノード同士のrank(順位)を書くには新しい計算方法を使います。
ファイルの冒頭でnewrank=trueとして、この機能を有効にします。

graph physical_net_topology {
    newrank=true

下から5,6行目にある以下の記載がrank(順位)を決めるものです。
rank=sameがノードのrankを同位にするためのキーワードです。
ただ、このキーワードを普通に(グローバルスコープで)書くと全ノードが同位になるので、波括弧でスコープを限定することで、「router0_e0とrouter1_e0は同位」、「l3sw0_e0とl3sw1_e0は同位」ということを定義しています。
cluster同士のrank定義ができればもっと簡潔に書けるとは思うのですが、できないのでノードにrankを付けています。

    {rank=same; router0_e0; router1_e0}
    {rank=same; l3sw0_e0; l3sw1_e0}

ややこしいことにrank=sameがsubgraph(cluster)の中にも増えています。

    subgraph cluster_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
        rank=same
    }

古い計算方法(newrank=false)の場合はsubgraphの中だけでrankを決めて、それからsubgraphの外側のrankを決めていました。新しい計算方法(newrank=true)にすると、subgraphで囲まれていることは無視して全ノード達を対象にrank決めをします。
…何を言っているかよく分からないですよね。ちょっと下の図を見てみてください。subgraphの中のsame=rankを消したものです。

上図は

  • router0_e0とrouter1_e0は同位(rank=same)
  • l3sw0_e0とl3sw1_e0は同位(rank=same)
  • router0_e2の1段下(1 rank下)がl3sw0_e2(「--」で結ばれている)
  • router1_e2の1段下(1 rank下)がl3sw1_e2(「--」で結ばれている)

という条件を満たしています。
先ほど、「subgraphで囲まれていることは無視して全ノード達を対象にrank決め」と述べたのは上記4つの条件がない(制約がない)ノードはランキングが最上位になり1段目に配置される、ということです。

subgraphの中にrank=sameを書くことで以下の制約を追加することができます。

  • subgraph cluster_router0の全ノード(全ネットワークインターフェイス)は同位
  • subgraph cluster_router1の全ノード(全ネットワークインターフェイス)は同位
  • subgraph cluster_l3sw0の全ノード(全ネットワークインターフェイス)は同位
  • subgraph cluster_l3sw1の全ノード(全ネットワークインターフェイス)は同位

完成

router, l3sw以外にもいくつかのネットワーク機器を追加してみました。色とか線の太さもいじっています。
冒頭の図を書くためのコードは以下の通りです。

graph physical_net_topology {
    newrank=true
    graph [
        fontname="Helvetica Neue, Helvetica,Arial,sans-serif"
        ranksep=1.5
    ]
    node [
        fontname="Helvetica Neue, Helvetica,Arial,sans-serif"
        style=filled, fillcolor=lightyellow
        margin="0.08"
    ]
    edge [
        fontname="Helvetica Neue, Helvetica,Arial,sans-serif"
        style=bold, color=chocolate
    ]
    subgraph cluster_router0 {
        label = router0
        router0_e0 [shape=box, label="e0"]
        router0_e2 [shape=box, label="e2"]
        rank=same
    }
    subgraph cluster_router1 {
        label = router1
        router1_e0 [shape=box, label="e0"]
        router1_e2 [shape=box, label="e2"]
        rank=same
    }
    subgraph cluster_firewall0 {
        label = firewall0
        firewall0_e0 [shape=box, label="e0"]
        rank=same
    }
    subgraph cluster_firewall1 {
        label = firewall1
        firewall1_e0 [shape=box, label="e0"]
        rank=same
    }
    subgraph cluster_lb0 {
        label = lb0
        lb0_e0 [shape=box, label="e0"]
        rank=same
    }
    subgraph cluster_lb1 {
        label = lb1
        lb1_e0 [shape=box, label="e0"]
        rank=same
    }
    subgraph cluster_l3sw0 {
        label = l3sw0
        l3sw0_e0 [shape=box, label="e0"]
        l3sw0_e1 [shape=box, label="e1"]
        l3sw0_e2 [shape=box, label="e2"]
        l3sw0_e3 [shape=box, label="e3"]
        l3sw0_e4 [shape=box, label="e4"]
        l3sw0_e5 [shape=box, label="e5"]
        rank=same
    }
    subgraph cluster_l3sw1 {
        label = l3sw1
        l3sw1_e0 [shape=box, label="e0"]
        l3sw1_e1 [shape=box, label="e1"]
        l3sw1_e2 [shape=box, label="e2"]
        l3sw1_e3 [shape=box, label="e3"]
        l3sw1_e4 [shape=box, label="e4"]
        l3sw1_e5 [shape=box, label="e5"]
        rank=same
    }
    subgraph cluster_l2sw0 {
        label = l2sw0
        l2sw0_e0 [shape=box, label="e0"]
        l2sw0_e1 [shape=box, label="e1"]
        l2sw0_e2 [shape=box, label="e2"]
        l2sw0_e3 [shape=box, label="e3"]
        l2sw0_e4 [shape=box, label="e4"]
        rank=same
    }
    subgraph cluster_l2sw1 {
        label = l2sw1
        l2sw1_e0 [shape=box, label="e0"]
        l2sw1_e1 [shape=box, label="e1"]
        l2sw1_e2 [shape=box, label="e2"]
        l2sw1_e3 [shape=box, label="e3"]
        l2sw1_e4 [shape=box, label="e4"]
        rank=same
    }
    subgraph cluster_host1 {
        label = host1
        host1_eth0 [shape=box, label="eth0"]
        host1_eth1 [shape=box, label="eth1"]
        rank=same
    }
    subgraph cluster_host2 {
        label = host2
        host2_eth0 [shape=box, label="eth0"]
        host2_eth1 [shape=box, label="eth1"]
        rank=same
    }

    // place redundant pair devices on same rank
    {rank=same; router0_e0; router1_e0}
    {rank=same; firewall0_e0; firewall1_e0}
    {rank=same; lb0_e0; lb1_e0}
    {rank=same; l3sw0_e0; l3sw1_e0}
    {rank=same; l2sw0_e3; l2sw1_e3}
    {rank=same; host1_eth0; host2_eth0}

    // place ports in order
    router0_e0 -- router0_e2 [style=invisible]
    router1_e0 -- router1_e2 [style=invisible]
//    l3sw0_e0 -- l3sw0_e1 -- l3sw0_e2 -- l3sw0_e3 -- l3sw0_e4 -- l3sw0_e5 [style=invisible]
//    l3sw1_e0 -- l3sw1_e1 -- l3sw1_e2 -- l3sw1_e3 -- l3sw1_e4 -- l3sw1_e5 [style=invisible]
//    l2sw0_e0 -- l2sw0_e1 -- l2sw0_e2 -- l2sw0_e3 -- l2sw0_e4 [style=invisible]
//    l2sw1_e0 -- l2sw1_e1 -- l2sw1_e2 -- l2sw1_e3 -- l2sw1_e4 [style=invisible]
    host1_eth0 -- host1_eth1 [style=invisible]
    host2_eth0 -- host2_eth1 [style=invisible]

    // cables
    router0_e0 -- router1_e0
    router0_e2 -- l3sw0_e2
    router1_e2 -- l3sw1_e2
    firewall0_e0 -- l3sw0_e3
    firewall1_e0 -- l3sw1_e3
    l3sw0_e4 -- lb0_e0
    l3sw1_e4 -- lb1_e0
    l3sw0_e5 -- l2sw0_e2
    l3sw1_e5 -- l2sw1_e2
    l3sw0_e0 -- l3sw1_e0
    l3sw0_e1 -- l3sw1_e1
    l2sw0_e0 -- l2sw1_e0
    l2sw0_e1 -- l2sw1_e1
    l2sw0_e3 -- host1_eth0
    l2sw1_e3 -- host1_eth1
    l2sw0_e4 -- host2_eth0
    l2sw1_e4 -- host2_eth1
}

上記のコードは一部コメントアウトしている箇所があります。
ネットワークインターフェイスの左右の並び順についてのところです。

subgraph内の同rankのノードを左右にどう並べるかはGraphvizがよしなに計算してくれます。
今回の図だと何もしないと以下の通りです。

これをe0を左、e2を右にするには以下の行を追加してあげればOKです。router0_e0とrouter0_e2
を線で結ぶ、その線は不可視にする、という意味です。

    router0_e0 -- router0_e2 [style=invisible]

l2sw, l3swも同様に以下のコードを追加すれば順番を並び替えてくれるはずです。

    l3sw0_e0 -- l3sw0_e1 -- l3sw0_e2 -- l3sw0_e3 -- l3sw0_e4 -- l3sw0_e5 [style=invisible]
    l3sw1_e0 -- l3sw1_e1 -- l3sw1_e2 -- l3sw1_e3 -- l3sw1_e4 -- l3sw1_e5 [style=invisible]
    l2sw0_e0 -- l2sw0_e1 -- l2sw0_e2 -- l2sw0_e3 -- l2sw0_e4 [style=invisible]
    l2sw1_e0 -- l2sw1_e1 -- l2sw1_e2 -- l2sw1_e3 -- l2sw1_e4 [style=invisible]

ただ、出来上がった図を見ると、何故かl3swのe1同士、l2swのe0, e1同士を結ぶ線が2本になってしまいました(l3swのe0同士は期待通り1本です)。

この謎が解けなかったのでコメントアウトしたものが冒頭の図です。
また、l3sw1のe4, e5の並び順が逆になっているのを見てもらうと分かるように、invisibleの線で結ぶというのはGraphvizのノード配置計算をする時の「ヒント」です。並び順を強制するものではありません。その他のノードとの接続状況によっては期待通りの順番にならないこともあります。

この謎を解いてみようという奇特な方はこちらへどうぞ

関連記事

脚注
  1. ここで言う「グラフ」は棒グラフや折れ線グラフの「グラフ」ではなくてグラフ理論の「グラフ」です ↩︎

Discussion