Batfish を使ってネットワーク構成を可視化してみよう (3)

## 前回からのつづき

これまで [可視化してみよう(1)](https://qiita.com/corestate55/items/8a39af553785fd77c20a)・[可視化してみよう(2)](https://qiita.com/corestate55/items/9d8023eb19637f9bbd1e) では、NW機器コンフィグから "どうにかしてデータを取り出し"、RFC8345 な形にまとめたものをどうやって可視化するのか、という話をしました。今回は可視化の前処理、Batfish から "どうにかして NW 構成のデータを取り出す" ところと、ネットワークトポロジのデータに組みなおすところを解説します。

[2021-03-24追記]

* :warning: 後半部、実際にデータを出力するためのスクリプト類については後から変更を加えています。
    * トポロジデータ(ファイル)名、`bf_l3trial` → `bf_l3ex` (layer3 example) で読み替えてください。
* [別途作成したREADME](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3trial/README.md) を参照してください。

## データ処理の流れ

こんな感じです。対象となるネットワークは  [pybatfish/jupyter\_notebooks/networks/example at master · batfish/pybatfish · GitHub](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks/networks/example) にあるやつですね。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/f5348536-e255-e0fe-434d-dd9555fe512f.png)

* Batfish を使って config からデータを抜き出す
  * ここは python script (pybatfish)
  * 図では Data table ってなってますけど、今回はいくつかの CSV ファイルに落としてます。
* Data table をもとに RFC8345 のトポロジデータを組み立てる
  * ここで ruby script になります。というのも、[RFC8345 データを作るための DSL を netomox という名前のツールで](https://github.com/corestate55/netomox)作っていて、それを使いたいから。
* 最後に、できあがった RFC8345 Topology Data を可視化(ブラウザ)・表示
  * 前回までで紹介したところです。

## Batfish でトポロジデータを取り出す

以下、Ubuntu18.10 つかってます。

### Batfish のセットアップ
まずは batfish を立てます。Docker でサクッと立てましょう。

* [[Batfish] 単体の Docker イメージと pybatfish の環境構築方法 - てくなべ (tekunabe)](https://tekunabe.hatenablog.jp/entry/2018/10/26/batfish_pybatfish)
* [ネットワークコンフィグ検証ツールBatfish使ってみた](https://ccieojisan.net/post-1803/)

以下の例では all-in-one 版をつかっています。(jupyter notebook 込みの方が、後から出てくる pybatfish の query でどんな情報が取れるのかとかのチェックが簡単だったから。)

```bash

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) を用意します。

```bash
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
(略)
```

[pybatfish のインストール](https://github.com/batfish/pybatfish/blob/master/README.md)

```bash
(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 しておきます。

```bash
hagiwara@dev01:~/batfish$ git clone https://github.com/batfish/pybatfish.git
```

### pybatfish で Batfish にクエリを投げる

あとは pybatfish をつかって必要な query を投げて情報を取り出しましょう。どんなクエリでどんな情報を取り出せるのか、というのを試すのには all-in-one docker image にはいっている jupyter notebook を使うのが便利です。どんなクエリがあるのかをまとめてくれてる人もいるのでその辺も参照しましょう。

* [BatfishのQuestion一覧と概要説明 - Qiita](https://qiita.com/tech_kitara/items/be71005ad7b5091d25a4)
* [Available questions — pybatfish 0.36.0 documentation](https://pybatfish.readthedocs.io/en/latest/questions.html)

なお、(私のような) python になじみがない人向けにちょっと補足をしておくと、pybatfish では query responce は `pybatfish.datamodel.answer.table.TableAnswer` などの class で返ってきます。

```python
(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](https://pandas.pydata.org/pandas-docs/stable/reference/frame.html) にしてくれる。そうなると各種データ処理が pandas の枠組みでいろいろやれるようになるので、あとはそっちに則ってデータ処理をしていく、という形です。ただ、一部のクエリでは `frame()` が実装されていないものもあるみたいです。

というのを踏まえてミニマムな query を投げるスクリプトを書くとこんな感じ。

```python

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 なげるスクリプト](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3trial/exec_l3queries.py)
* [結果(CSV)](https://github.com/corestate55/netomox-examples/tree/develop/model_defs/bf_l3trial/csv/example)
  * `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](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3ex.rb) と、[そこから require しているスクリプト](https://github.com/corestate55/netomox-examples/tree/develop/model_defs/bf_l3trial) ですね。スクリプトにある通り [netomox というライブラリ](https://github.com/corestate55/netomox)を使っているのですが、これは私が RFC8345 データを組み立てるために作った DSL になります。(手書きで JSON データ組み立てるのは地獄のような面倒くささなので。)

たとえば [Layer3 トポロジのデータを作るスクリプト](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3trial/layer_l3.rb)から DSL 部分を抜粋するとこんな感じですね。`node`, `term_point`, `link`, `attribute` とかが DSL のメソッドになっていて、これでトポロジデータを定義してます。……まあ rubocop に怒られててそんなにきれいに書けてない気はしますが、データ定義の記述量としてはだいぶ楽になります。

```ruby:layer_l3.rb

  # 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 にして出力](https://github.com/corestate55/netoviz/blob/develop/static/model/bf_l3ex.json)しています。

### 可視化する

可視化の所は[前回の通り](https://qiita.com/corestate55/items/9d8023eb19637f9bbd1e)です。

## モデルのチェック(validation)

さて、途中に書いたとおり、最初は可視化お試し用のデータ (JSON) を手書きで書いていたのですが、あまりにも面倒くさいので DSL を作ったのでした。とはいえ、DSL は少ない記述量でデータをかけるだけなので、そもそも間違った図を書くリスクがあります。例えば、ホスト名を間違えたり、ポート (term point) の番号を間違えたり……。そのため、できあがったデータの整合性をチェックして、怪しい箇所には警告を出すチェック機能を netomox に仕込んであります。

今回使っている[チュートリアル用 config](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks/networks/example) なんですが、そのチェックで引っかかるところがあったので、モデルベースにネットワーク構成を考えることのメリットのひとつとして例示してみたいと思います。

チュートリアル用の config ですが、query 例示のためか、一見意味をなさないコンフィグ(パラメータ)が入ってるところがあるんですよね。で、トポロジデータに対するチェック事項のひとつに、ポート (term point, TP) とリンクの対応関係のチェックというのをいれてあります。データモデル上、リンクは TP が端点になり、かつひとつの TP はひとつのリンクと接続される、ということになっています。なので、リンクがどの TP を参照しているかカウントを取っていったときに、TP の参照カウントが 2 [^link] 以外の場合は、リンクを間違えて設定している恐れがある……というチェックです。

[^link]: RFC8345 のモデル定義上、リンクは片方向リンクで表現されます。NW 図のリンクはほとんどの場合双方向リンクなので、TP は行き帰りの 2 回参照されます。

結果書いちゃいますが、これで怪しい IP が設定されている使われていないポートが発見できました。

```bash

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 をよけると、リンクが設定されていない(使われていない)インタフェースが引っかかる、と。見ての通り、チュートリアル用のダミーデータみたいな設定ですね。

<!-- ![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/353fa77c-6e1e-d81a-4e13-f0c1e8b3f622.png) -->
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/353fa77c-6e1e-d81a-4e13-f0c1e8b3f622.png" width="70%">

Batfish で `edges` query からトポロジ情報を引くと、リンク接続のあるインタフェースだけが出てくるので、これは `ipOwners` query から引っ張ったポート情報を元に設定しているから起きた現象 [^ipowners] だと思います。こうした、思いがけないミスや自分が気づかなかったことの発見も、データとして構成情報を定義することで、自動的にチェックしたりテストしたり、いろいろできるようになるんじゃないかと思います。

[^ipowners]: Loopback インタフェースは `edges` クエリの結果としては出てこないので、Layer3 node のインタフェース (TP) は `ip_owners` クエリをもとに作ってるはず。

## おわりに

### Batfish によるデータ取得

可視化の前処理、NW 機器コンフィグからトポロジ情報を取り出して RFC8345 なデータ形式に変換するところまでの流れを書きました。

元のネットワークに関する知識に基づいてデータ変換とかは省略しちゃってるところがいくつかあるので、あまり一般的なツールにはなっていません。例えば、VRF がない (default VRF = L3 Router) 前提とか、マルチエリア OSPF になってないので Area 間接続をチェックしてないとか、元のネットワークがこうなってるからこれでいいよね、という省略ですね。そのあたりはまあ実験なのでご容赦を……。それでも、batfish で結構いろいろなデータを抽出できて、それを元にいろんな処理(今回は可視化)ができるんだろうな……ということを見てもらえると良いかなと思います。

いやホント batfish すごいですね。トポロジ関係とかネットワークの構造に関する query は、グラフデータベースにデータ入れたりすればできそうな項目もあるんですが、batfish は最初から「ネットワークのための」チェックしますからね。一般的(抽象的)なグラフに対する処理よりももっとダイレクトで「エンジニアが知りたいこと」がチェックできる。

### 設計・構築・運用のプロセス

見ての通り、config から図を作ってみたわけですが、図と config が紐付くようになっているわけで、図から config を起こすというのも理屈の上ではできるわけです。そこで、オマケとしてモデル検査 (validation) の話をちょっと入れました。ネットワークでは常に対向・隣接する機器間の整合性が問題になります。何と何の整合性を合わせなければいけないかは図を描いてチェックしないとよくわからないですし、人力レビューではなかなか気づかない見落としもあります。そうしたあれこれをモデル/データとしてちゃんと表現できれば、各チェック事項(ポリシ)に従っているかどうかみたいなのをまとめてチェックできる。こういうのを積み重ねて、設計段階で「全体のトポロジや構成を絵で描く」→ その中で検査をする → 実際のデプロイ、みたいな形になっていくといいなあと思っています。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/c3659a27-85ae-c19a-4c9f-ca8d146125ac.png)

### 将来象

そしてゆくゆくは、上の図のように、構成図を描く(モデルを作る)とそれに応じたネットワークができあがる形を……。そのために「人が理解しやすい形」を既存の NW や config から作る、というところをやっています。まずは「こういう表現形式 (データ構造) が理解しやすいよね」というモノを探る → そこから「図を元に作る」方へシフトしていけたらいいなと。RFC8345 は汎用的なデータモデルで、それで実際のネットワークをどう表現するかというのは特に規定がない。理解や表現が難しい構成要素……例えば、冗長化されるもの、仮想化されるもの、オーバーレイされるもの、各要素の依存関係……などをどういう形で表せばよいのか。これはまだまだプラクティスが必要だと思うので、いろんな試みがやれるといいなあと思っています。