前回からのつづき
これまで 可視化してみよう(1)・可視化してみよう(2) では、NW機器コンフィグから "どうにかしてデータを取り出し"、RFC8345 な形にまとめたものをどうやって可視化するのか、という話をしました。今回は可視化の前処理、Batfish から "どうにかして NW 構成のデータを取り出す" ところと、ネットワークトポロジのデータに組みなおすところを解説します。
[2021-03-24追記]
-
後半部、実際にデータを出力するためのスクリプト類については後から変更を加えています。
- トポロジデータ(ファイル)名、
bf_l3trial
→bf_l3ex
(layer3 example) で読み替えてください。
- トポロジデータ(ファイル)名、
- 別途作成したREADME を参照してください。
データ処理の流れ
こんな感じです。対象となるネットワークは pybatfish/jupyter_notebooks/networks/example at master · batfish/pybatfish · GitHub にあるやつですね。
- Batfish を使って config からデータを抜き出す
- ここは python script (pybatfish)
- 図では Data table ってなってますけど、今回はいくつかの CSV ファイルに落としてます。
- Data table をもとに RFC8345 のトポロジデータを組み立てる
- ここで ruby script になります。というのも、RFC8345 データを作るための DSL を netomox という名前のツールで作っていて、それを使いたいから。
- 最後に、できあがった RFC8345 Topology Data を可視化(ブラウザ)・表示
- 前回までで紹介したところです。
Batfish でトポロジデータを取り出す
以下、Ubuntu18.10 つかってます。
Batfish のセットアップ
まずは batfish を立てます。Docker でサクッと立てましょう。
以下の例では all-in-one 版をつかっています。(jupyter notebook 込みの方が、後から出てくる pybatfish の query でどんな情報が取れるのかとかのチェックが簡単だったから。)
hagiwara@dev01:~$ sudo docker pull batfish/allinone
(略)
hagiwara@dev01:~$ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
batfish/allinone latest 675d04b75064 13 days ago 948MB
hello-world latest fce289e99eb9 3 months ago 1.84kB
hagiwara@dev01:~$ sudo docker run -p 8888:8888 -p 9997:9997 -p 9996:9996 batfish/allinone
[sudo] hagiwara のパスワード:
[I 04:04:50.787 NotebookApp] Writing notebook server cookie secret to /data/.local/share/jupyter/runtime/notebook_cookie_secret
[I 04:05:05.559 NotebookApp] Serving notebooks from local directory: /notebooks
[I 04:05:05.559 NotebookApp] The Jupyter Notebook is running at:
[I 04:05:05.559 NotebookApp] http://(acb2a8f8c8a8 or 127.0.0.1):8888/?token=59188a6b516ba19661bfcd233fd95a11f01fd7946a27f01b
[I 04:05:05.559 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 04:05:05.697 NotebookApp] No web browser found: could not locate runnable browser.
[C 04:05:05.697 NotebookApp]
To access the notebook, open this file in a browser:
file:///data/.local/share/jupyter/runtime/nbserver-7-open.html
Or copy and paste one of these URLs:
http://(acb2a8f8c8a8 or 127.0.0.1):8888/?token=59188a6b516ba19661bfcd233fd95a11f01fd7946a27f01b
tcp/8888 が jupyter notebook, tcp/9996-9997 が batfish ですね。
Python 環境の準備と pybatfish のインストール
Batfish に config 食わせて各種 query 投げるために pybatfish を使うのですが、その前に python 環境 (venv) を用意します。
hagiwara@dev01:~/batfish$ sudo apt install python3-venv
(略)
hagiwara@dev01:~/batfish$ python3 -m venv bf-venv
hagiwara@dev01:~/batfish$ . bf-venv/bin/activate
(bf-venv) hagiwara@dev01:~/batfish$ python --version
Python 3.6.8
(bf-venv) hagiwara@dev01:~/batfish$ pip --version
pip 9.0.1 from /home/hagiwara/batfish/bf-venv/lib/python3.6/site-packages (python 3.6)
(bf-venv) hagiwara@dev01:~/batfish$ pip install wheel
(略)
(bf-venv) hagiwara@dev01:~/batfish$ python3 -m pip install --upgrade git+https://github.com/batfish/pybatfish.git
Collecting git+https://github.com/batfish/pybatfish.git
Cloning https://github.com/batfish/pybatfish.git to /tmp/pip-edqkh06e-build
(略)
(bf-venv) hagiwara@dev01:~/batfish$
あと batfish に食わせたいデータ (config) を用意しておけば良いのですが、今回は pybatfish のチュートリアルをそのまま使うので clone しておきます。
hagiwara@dev01:~/batfish$ git clone https://github.com/batfish/pybatfish.git
pybatfish で Batfish にクエリを投げる
あとは pybatfish をつかって必要な query を投げて情報を取り出しましょう。どんなクエリでどんな情報を取り出せるのか、というのを試すのには all-in-one docker image にはいっている jupyter notebook を使うのが便利です。どんなクエリがあるのかをまとめてくれてる人もいるのでその辺も参照しましょう。
なお、(私のような) python になじみがない人向けにちょっと補足をしておくと、pybatfish では query responce は pybatfish.datamodel.answer.table.TableAnswer
などの class で返ってきます。
(bf-venv) hagiwara@dev01:~/batfish$ python
Python 3.6.8 (default, Apr 9 2019, 04:59:38)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pybatfish.client.commands import *
>>> from pybatfish.question.question import load_questions, list_questions
>>> from pybatfish.question import bfq
>>> load_questions()
Successfully loaded 57 questions from remote
>>> bf_init_snapshot('pybatfish/jupyter_notebooks/networks/example')
(略)
Default snapshot is now set to ss_c3c54c28-96ab-4158-b827-8aca3dc531d1
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... Wed May 1 13:38:43 2019 JST Begin job.
status: TERMINATEDNORMALLY
.... Wed May 1 13:38:43 2019 JST Begin job.
'ss_c3c54c28-96ab-4158-b827-8aca3dc531d1'
>>>
>>> ip_owners_ans = bfq.ipOwners().answer()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... Wed May 1 13:54:04 2019 JST Begin job.
>>>
>>> type(ip_owners_ans)
<class 'pybatfish.datamodel.answer.table.TableAnswer'>
>>>
これの frame()
メソッドを呼ぶと pandas.DataFrame にしてくれる。そうなると各種データ処理が pandas の枠組みでいろいろやれるようになるので、あとはそっちに則ってデータ処理をしていく、という形です。ただ、一部のクエリでは frame()
が実装されていないものもあるみたいです。
というのを踏まえてミニマムな query を投げるスクリプトを書くとこんな感じ。
from pybatfish.client.commands import *
from pybatfish.question.question import load_questions
from pybatfish.question import bfq
snapshot_dir = 'pybatfish/jupyter_notebooks/networks/example'
snapshot_name = 'bf_example_snapshot'
if __name__ == '__main__':
# load question
load_questions()
# init snapshot
bf_init_snapshot(snapshot_dir, name=snapshot_name)
# query
ip_owners = bfq.ipOwners().answer()
# save
with open('ip_owners.csv', 'w') as outfile:
outfile.write(ip_owners.frame().to_csv())
Query (ipOwners()
) を実行して得られた結果を CSV file で保存してます。
IP/Topology 情報のクエリ
今回は bgp/ospf/layer3 でのトポロジ情報を出すのですが、最終的には以下のクエリでデータを取り出しています。
- Query なげるスクリプト
-
結果(CSV)
-
ipOwners
: 各ルータ(VRF) が持つインタフェースとそのIPアドレスの情報 -
routes
: 各ルータ(VRF)の経路情報 -
edges
, 引数でプロトコルを渡してプロトコルごとの隣接情報を出すことができます- BGP
- OSPF
- Layer3
-
bgpProcessConfiguration
: BGP プロセスの設定情報 -
ospfAreaConfiguration
: OSPF プロセス (Area) の設定情報
-
スクリプト見てわかる通り、今回 batfish query では特にデータの加工をしていません。素直に query なげて CSV に保存する処理だけにしてます。そして pandas なクラスに wrap してくれるよ、という便利機能があるのを知りつつデータ加工は ruby でやってます。
データを加工して RFC8345 トポロジデータを作る
データ変換スクリプトは bf_l3trial.rb bf_l3ex.rb と、そこから require しているスクリプト ですね。スクリプトにある通り netomox というライブラリを使っているのですが、これは私が RFC8345 データを組み立てるために作った DSL になります。(手書きで JSON データ組み立てるのは地獄のような面倒くささなので。)
たとえば Layer3 トポロジのデータを作るスクリプトから DSL 部分を抜粋するとこんな感じですね。node
, term_point
, link
, attribute
とかが DSL のメソッドになっていて、これでトポロジデータを定義してます。……まあ rubocop に怒られててそんなにきれいに書けてない気はしますが、データ定義の記述量としてはだいぶ楽になります。
# rubocop:disable Metrics/MethodLength
def make_layer3_layer_nodes(nws)
@node_interfaces_table.each_pair do |node, interfaces|
prefixes = routes_of(node, /^(?!.*(bgp|ospf)).+$/) # exclude bgp,ospf
nws.network('layer3').register do
node node do
interfaces.each do |tp|
term_point tp[:interface] do
attribute(ip_addrs: ["#{tp[:ip]}/#{tp[:mask]}"])
end
end
attribute(prefixes: prefixes, flags: ['layer3'])
end
end
end
end
# rubocop:enable Metrics/MethodLength
def make_layer3_layer_links(nws)
@links.each do |link_row|
src = link_row[:source]
dst = link_row[:destination]
nws.network('layer3').register do
link src[:node], src[:interface], dst[:node], dst[:interface]
end
end
end
def make_layer3_layer(nws)
nws.register do
network 'layer3' do
type Netomox::NWTYPE_L3
end
end
make_layer3_layer_nodes(nws)
make_layer3_layer_links(nws)
end
これは最終的には RFC8345/8346 で定義されたデータモデルに基づく階層型のデータ構造を作っているだけで、最後は JSON にして出力しています。
可視化する
可視化の所は前回の通りです。
モデルのチェック(validation)
さて、途中に書いたとおり、最初は可視化お試し用のデータ (JSON) を手書きで書いていたのですが、あまりにも面倒くさいので DSL を作ったのでした。とはいえ、DSL は少ない記述量でデータをかけるだけなので、そもそも間違った図を書くリスクがあります。例えば、ホスト名を間違えたり、ポート (term point) の番号を間違えたり……。そのため、できあがったデータの整合性をチェックして、怪しい箇所には警告を出すチェック機能を netomox に仕込んであります。
今回使っているチュートリアル用 config なんですが、そのチェックで引っかかるところがあったので、モデルベースにネットワーク構成を考えることのメリットのひとつとして例示してみたいと思います。
チュートリアル用の config ですが、query 例示のためか、一見意味をなさないコンフィグ(パラメータ)が入ってるところがあるんですよね。で、トポロジデータに対するチェック事項のひとつに、ポート (term point, TP) とリンクの対応関係のチェックというのをいれてあります。データモデル上、リンクは TP が端点になり、かつひとつの TP はひとつのリンクと接続される、ということになっています。なので、リンクがどの TP を参照しているかカウントを取っていったときに、TP の参照カウントが 2 1 以外の場合は、リンクを間違えて設定している恐れがある……というチェックです。
結果書いちゃいますが、これで怪しい IP が設定されている使われていないポートが発見できました。
hagiwara@dev01:~/nwmodel/netomox-examples$ bundle exec netomox check static/model/bf_l3trial.json | jq '.[] | select(.checkup == "link reference count of terminal-point").messages[] | select(.path|test("__Lo0")|not)'
{
"severity": "warn",
"path": "layer3__as1border2__Gi2/0",
"message": "irregular ref_count:0"
}
{
"severity": "warn",
"path": "layer3__as3core1__Gi2/0",
"message": "irregular ref_count:0"
}
{
"severity": "warn",
"path": "layer3__as3core1__Gi3/0",
"message": "irregular ref_count:0"
}
hagiwara@dev01:~/nwmodel/netomox-examples$
図 (as3core1, Gi3/0) だとこんな所ですね。ポートはありますがリンクされていません。ちなみに、jq でフィルタかけてある通り、このクエリでは主に loopback interface が引っかかります。(loopback は当然リンクがない。) Loopback をよけると、リンクが設定されていない(使われていない)インタフェースが引っかかる、と。見ての通り、チュートリアル用のダミーデータみたいな設定ですね。
Batfish で edges
query からトポロジ情報を引くと、リンク接続のあるインタフェースだけが出てくるので、これは ipOwners
query から引っ張ったポート情報を元に設定しているから起きた現象 2 だと思います。こうした、思いがけないミスや自分が気づかなかったことの発見も、データとして構成情報を定義することで、自動的にチェックしたりテストしたり、いろいろできるようになるんじゃないかと思います。
おわりに
Batfish によるデータ取得
可視化の前処理、NW 機器コンフィグからトポロジ情報を取り出して RFC8345 なデータ形式に変換するところまでの流れを書きました。
元のネットワークに関する知識に基づいてデータ変換とかは省略しちゃってるところがいくつかあるので、あまり一般的なツールにはなっていません。例えば、VRF がない (default VRF = L3 Router) 前提とか、マルチエリア OSPF になってないので Area 間接続をチェックしてないとか、元のネットワークがこうなってるからこれでいいよね、という省略ですね。そのあたりはまあ実験なのでご容赦を……。それでも、batfish で結構いろいろなデータを抽出できて、それを元にいろんな処理(今回は可視化)ができるんだろうな……ということを見てもらえると良いかなと思います。
いやホント batfish すごいですね。トポロジ関係とかネットワークの構造に関する query は、グラフデータベースにデータ入れたりすればできそうな項目もあるんですが、batfish は最初から「ネットワークのための」チェックしますからね。一般的(抽象的)なグラフに対する処理よりももっとダイレクトで「エンジニアが知りたいこと」がチェックできる。
設計・構築・運用のプロセス
見ての通り、config から図を作ってみたわけですが、図と config が紐付くようになっているわけで、図から config を起こすというのも理屈の上ではできるわけです。そこで、オマケとしてモデル検査 (validation) の話をちょっと入れました。ネットワークでは常に対向・隣接する機器間の整合性が問題になります。何と何の整合性を合わせなければいけないかは図を描いてチェックしないとよくわからないですし、人力レビューではなかなか気づかない見落としもあります。そうしたあれこれをモデル/データとしてちゃんと表現できれば、各チェック事項(ポリシ)に従っているかどうかみたいなのをまとめてチェックできる。こういうのを積み重ねて、設計段階で「全体のトポロジや構成を絵で描く」→ その中で検査をする → 実際のデプロイ、みたいな形になっていくといいなあと思っています。
将来象
そしてゆくゆくは、上の図のように、構成図を描く(モデルを作る)とそれに応じたネットワークができあがる形を……。そのために「人が理解しやすい形」を既存の NW や config から作る、というところをやっています。まずは「こういう表現形式 (データ構造) が理解しやすいよね」というモノを探る → そこから「図を元に作る」方へシフトしていけたらいいなと。RFC8345 は汎用的なデータモデルで、それで実際のネットワークをどう表現するかというのは特に規定がない。理解や表現が難しい構成要素……例えば、冗長化されるもの、仮想化されるもの、オーバーレイされるもの、各要素の依存関係……などをどういう形で表せばよいのか。これはまだまだプラクティスが必要だと思うので、いろんな試みがやれるといいなあと思っています。