いまからはじめるpybatfish

## はじめに

[Batfish](https://www.batfish.org/) というネットワーク機器のコンフィグレーションファイルを解析してくれる便利ツールがあります。batfish 自体はクライアント・サーバ型で使うようになっていて、API は REST なんですが、その python wrapper として [batfish/pybatfish](https://github.com/batfish/pybatfish) があります。ここでは batfish の基本的な「管理単位」とそれを pybatfish から扱うための方法を step-by-step に見ていきます。

batfish および pybatfish については過去何回か記事を書いているのですが、私が書いているスクリプトがちょっと古くてですね。ちゃんと現時点版でのあれこれをまとめておこうという意図もあります。

## セットアップ

オフィシャルのチュートリアルでは [all-in-one 版のコンテナ](https://hub.docker.com/r/batfish/allinone)を持ってきて、同梱されている jupyter notebook 上で操作しよう、という流れになっています。それはそれでいいのですが、今回は python script で batfish 操作していくための土台として考えているので、もうちょっとベタな方法で見ていきます。(と言ってもたいした違いはないですが)

all-in-one 版ではなく通常版のコンテナを落とします。jupyter とかが同梱されていないので軽量です。

```bash
docker pull batfish/batfish
```

2022-01-22 時点 latests ではこれくらいサイズが違います。

```
$ docker image ls | grep batfish
batfish/allinone          latest    5ba3c111cc7c   5 weeks ago     1.08GB
batfish/batfish           latest    bb872fd73111   5 weeks ago     403MB
```

pybatfish をインストールします。これは pip でインストールできます。(ここでは python/3.8.3, pip/21.3.8 を使っています)

```bash
pip install pybatfish
```

pybatfish 2021.11.4.1095 です

```
$ pip list | grep batfish
pybatfish         2021.11.4.1095
```

## pybatfish version に関する余談

このあと [Welcome to the Batfish documentation! — pybatfish 0.36.0 documentation](https://pybatfish.readthedocs.io/en/latest/) ってドキュメントを参照しながら書いてるんですが、この title のところに "0.36.0" ってありますよね。

pip install したバージョンと違うので何だろうというので探して見るんですが、[pybatfish · PyPI](https://pypi.org/project/pybatfish/#history) の Release history も [Releases · batfish/pybatfish](https://github.com/batfish/pybatfish/releases) も YYYY.MM.DD な日付ベースのバージョン設定なんですよ。じゃあ 0.36.0 ってどっから来たバージョンなんだと。おそらく[この `__init__.py`で定義してあるやつ](https://github.com/batfish/pybatfish/blob/master/pybatfish/__init__.py#L24)じゃないかと思うんですが (python package 作ったことないのでよくわかってない)、このファイルの last commit が “15 Oct 2019”…。

ということで、この 0.36.0 っていうのがあまり信用なりません。このバージョン番号を指定してもインストールはできるみたいなんですが、もし日付ベースのバージョンになっていなければ、いったん消して入れ直すほうが良いかもしれません。

```diff
$ pip install pybatfish==2021.11.4.1095
...省略...
Successfully installed pybatfish-2021.11.4.1095
$ pip list | grep batfish
pybatfish         2021.11.4.1095
```

## 分析対象の準備

あらかじめ、batfish で解析したいネットワーク機器の設定ファイルを用意します。batfish は決まったディレクトリ構成に基づいてデータを読むのと、”snapshot” という考え方があるので、まずはそれを押さえておきましょう。

- ref: [Interacting with the Batfish service — pybatfish 0.36.0 documentation](https://pybatfish.readthedocs.io/en/latest/notebooks/interacting.html#Packaging-snapshot-data)

### Snapshot

Batfish が分析する対象は、単一の NW 機器コンフィグファイルではなく、複数 (環境全体) の NW 機器コンフィグファイルです。「環境全体」を把握するために、NW 機器コンフィグファイル以外にも、物理 (L1) トポロジ情報、エンドポイント情報等のデータを同梱できます。これらのデータ一式をまとめて “snapshot” と呼びます。

1つの環境（ネットワーク）に対して、例えば設定変更作業の前後や日次など、複数の snapshot を設定できます。後ほど出てきますが、Batfish は下の図のような考え方で “snapshot” を持っています。

- batfish は複数の network を持つ
- network は複数の snapshot を持つ
- Batfish は 1 つの snapshot を指定して分析を実行する

![batfish_basics.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/dc3c1d9a-6510-c6d3-60a1-5865617f004c.png)


### ディレクトリ構成

snapshot ごとにディレクトリを用意し、その中に特定の名前でファイルやディレクトリを設置します。

- snapshot-directory/
    - configs/ (必須)
        - NW機器コンフィグファイルを置きます。
        - ref: [Format of vendor and supplemental data](https://pybatfish.readthedocs.io/en/latest/formats.html#device-configuration-formats)
    - hosts/
        - ホスト (endpoint) 設定ファイル
    - iptables/
        - ホスト (endpoint) の iptables 設定ファイル
    - batfish/
        - layer1_topology.json : L1トポロジ情報
        - isp_config.json : ISP (インターネット接続) 情報
        - runtime_data.json : インターフェスの実行時状態 (runtime state) 情報
        - external_bgp_announcements.json : 外部 AS との BGP 情報

configs 以外のディレクトリはオプショナルです。そこで設置するファイルについては [Format of vendor and supplemental data](https://pybatfish.readthedocs.io/en/latest/formats.html#supplemental-data-formats) を参照してください。(わたしも全部を使ったことがあるわけではないのであまり細かく説明できません…)

### Sample snapshots

…と、いうのを踏まえて今回はサンプルのコンフィグを使います。

- [corestate55/batfish-test-topology](https://github.com/corestate55/batfish-test-topology)
    - 別記事で使ったものなので、細かい中身は README に貼ってあるリンクからたどってください。

## pybatfish step-by-step

ref: [Interacting with the Batfish service — pybatfish 0.36.0 documentation](https://pybatfish.readthedocs.io/en/latest/notebooks/interacting.html)

### Batfish (server) の起動

Batfishコンテナを起動します。

```python
$ docker run -d --rm --name batfish -p 9996:9996 -p 9997:9997 batfish/batfish
59cf4ad5ca6dc67ee38e4a5e2f501949c8eca197db5b12e86f3a6cf14aa23dc7
$ docker container ls
CONTAINER ID   IMAGE             COMMAND                  CREATED          STATUS         PORTS                                                           NAMES
59cf4ad5ca6d   batfish/batfish   "java -XX:-UseCompre…"   10 seconds ago   Up 9 seconds   0.0.0.0:9996-9997->9996-9997/tcp, :::9996-9997->9996-9997/tcp   batfish
```

### pybatfish (client) の実行

[corestate55/batfish-test-topology](https://github.com/corestate55/batfish-test-topology) を clone して、そのディレクトリに移動して作業していきます。

```
$ git clone https://github.com/corestate55/batfish-test-topology.git
$ cd batfish-test-topology
```

python を対話モードで起動します。

```
$ python -i
Python 3.8.3 (default, Jan  1 2021, 16:39:20) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
```

pybatfish Session を作ります

```python
>>> from pybatfish.client.session import Session
>>> bf = Session()
>>>
```

`Session` の引数を何も指定しないと localhost に接続します。別途 batfish service を起動している場合は `host` 引数を指定します。(引数なしの場合は `Session(host="localhost")` と同等)

### snapshot の登録

次は分析したい NW機器コンフィグファイルを登録していくのですが、先に、snapshot を束ねる単位である network を設定しておきます。

```python
>>> # ネットワーク一覧を出してみるけどまだ何もない
>>> bf.list_networks()
[]
>>> # ネットワークを登録する
>>> bf.set_network("batfish-test-topology")
'batfish-test-topology'
>>> bf.list_networks()
['batfish-test-topology']
>>>
>>> # ネットワークの中に snapshot が...まだ入っていない
>>> bf.list_snapshots()
[]
```

では snapshot を登録しましょう。batfish-test-topology にはいくつかの snapshot が入っています。[そのうちの1つを](https://github.com/corestate55/batfish-test-topology/tree/develop/l2/sample3)登録してみます。

```python
>>> bf.init_snapshot('./l2/sample3', name="l2_sample3")
'l2_sample3'
>>> bf.list_snapshots()
['l2_sample3']
```

登録できました。

### 分析 (query) の実行

snapshot が登録できれば、対象とする snapshot を選択して分析 (batfish query) を実行できます。

ここでは L1 edges (物理リンク情報) を取得してみます。

```python
>>> bf.list_snapshots()
['l2_sample3']
>>> bf.set_snapshot('l2_sample3')
'l2_sample3'
>>> bf.q.edges(edgeType='layer1').answer().frame()
                        Interface               Remote_Interface
0                    host13[eth0]  switch2[GigabitEthernet1/0/3]
1                    host14[eth0]  switch2[GigabitEthernet1/0/4]
2                    host22[eth0]  switch1[GigabitEthernet1/0/6]
3   switch2[GigabitEthernet1/0/4]                   host14[eth0]
4          switch1[Port-channel1]         switch2[Port-channel1]
5          switch2[Port-channel1]         switch1[Port-channel1]
6                    host23[eth0]  switch2[GigabitEthernet1/0/7]
7   switch2[GigabitEthernet1/0/8]                   host24[eth0]
8   switch1[GigabitEthernet1/0/6]                   host22[eth0]
9                    host21[eth0]  switch1[GigabitEthernet1/0/5]
10  switch2[GigabitEthernet1/0/7]                   host23[eth0]
11                   host11[eth0]  switch1[GigabitEthernet1/0/1]
12                   host12[eth0]  switch1[GigabitEthernet1/0/2]
13                   host24[eth0]  switch2[GigabitEthernet1/0/8]
14  switch1[GigabitEthernet1/0/5]                   host21[eth0]
15  switch2[GigabitEthernet1/0/3]                   host13[eth0]
16  switch1[GigabitEthernet1/0/1]                   host11[eth0]
17  switch1[GigabitEthernet1/0/2]                   host12[eth0]
```

この query では pandas dataframe として結果を取得しています(`frame()`)。query によっては異なるオブジェクトで返ってきます。それらのデータオブジェクトを扱う際は対応するデータモデルをインポートしておく必要があります。

```python
from pybatfish.datamodel import *
from pybatfish.datamodel.answer import *
from pybatfish.datamodel.flow import *
```

### 異なる snapshot の登録・使い分け

`l2/sample3/batfish` ディレクトリの `layer1_topology.json`を編集して、host11-switch1 間のリンクを消します。(片方向ずつ 2 本セットで双方向になっている点に注意)

```diff
diff --git a/l2/sample3/batfish/layer1_topology.json b/l2/sample3/batfish/layer1_topology.json
index 5f482be..5260579 100644
--- a/l2/sample3/batfish/layer1_topology.json
+++ b/l2/sample3/batfish/layer1_topology.json
@@ -1,25 +1,5 @@
 {
   "edges": [
-    {
-      "node1": {
-        "hostname": "switch1",
-        "interfaceName": "GigabitEthernet1/0/1"
-      },
-      "node2": {
-        "hostname": "host11",
-        "interfaceName": "eth0"
-      }
-    },
-    {
-      "node1": {
-        "hostname": "host11",
-        "interfaceName": "eth0"
-      },
-      "node2": {
-        "hostname": "switch1",
-        "interfaceName": "GigabitEthernet1/0/1"
-      }
-    },
     {
       "node1": {
         "hostname": "switch1",
```

この状態で、別な snapshot 名をつけて登録してみましょう。

```python
>>> bf.init_snapshot('./l2/sample3', name="l2_sample3_sw1_gi101_off")
'l2_sample3_sw1_gi101_off'
>>> bf.list_snapshots()
['l2_sample3_sw1_gi101_off', 'l2_sample3']
```

同様に L1 edges を取得してみます。

```python
>>> bf.set_snapshot('l2_sample3_sw1_gi101_off')
'l2_sample3_sw1_gi101_off'
>>> bf.q.edges(edgeType='layer1').answer().frame()
                        Interface               Remote_Interface
0                    host13[eth0]  switch2[GigabitEthernet1/0/3]
1                    host14[eth0]  switch2[GigabitEthernet1/0/4]
2                    host22[eth0]  switch1[GigabitEthernet1/0/6]
3   switch2[GigabitEthernet1/0/4]                   host14[eth0]
4          switch1[Port-channel1]         switch2[Port-channel1]
5          switch2[Port-channel1]         switch1[Port-channel1]
6                    host23[eth0]  switch2[GigabitEthernet1/0/7]
7   switch2[GigabitEthernet1/0/8]                   host24[eth0]
8   switch1[GigabitEthernet1/0/6]                   host22[eth0]
9                    host21[eth0]  switch1[GigabitEthernet1/0/5]
10  switch2[GigabitEthernet1/0/7]                   host23[eth0]
11                   host12[eth0]  switch1[GigabitEthernet1/0/2]
12                   host24[eth0]  switch2[GigabitEthernet1/0/8]
13  switch1[GigabitEthernet1/0/5]                   host21[eth0]
14  switch2[GigabitEthernet1/0/3]                   host13[eth0]
15  switch1[GigabitEthernet1/0/2]                   host12[eth0]
>>>
```

`layer1_topology.json` で指定したとおり、host11-switch1 間のリンクが 2 本 (片方向ずつ、双方向)  見えなくなりました。snapshot `l2_sample3` と同じNW機器コンフィグファイルを使用していますが、L1 topology が違う別な snapshot として扱うことができています。

## おわりに

pybatfish の基本的な使い方をまとめてみました。[pybatfish v2021.11.04 tag のリリースノート](https://github.com/batfish/pybatfish/releases/tag/v2021.11.04)に “examples and documentation have been updated to use `Session`s” とあるので、おそらくこのへんから `Session` 中心の書き方に変わっています。

古い書き方は [legacy_docs](https://pybatfish.readthedocs.io/en/legacy_docs/notebooks/interacting.html) の方に残してあるみたいなんですが、`Session` が明示的に出てこなかったりします。ディレクトリ構成も追加データの json file が `batfish` ディレクトリじゃなくて snapshot ディレクトリ直下にあったりするかも。混乱しないように注意してください。

このあたりの管理方法等は変わってきているので、これから使う方はいま一度ドキュメントを参照するようにしてください。