BatfishでL2 Topologyを出せるかどうか調べてみる (1)

## はじめに

これまで、 [Batfish を使ってネットワーク構成を可視化してみよう](https://qiita.com/corestate55/items/8a39af553785fd77c20a) など Batfish を使ってコンフィグから情報を吸い出して可視化するというのをやってきました。そこでは  [pybatfish のリポジトリにあるチュートリアル用サンプルコンフィグ](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks/networks/example) を使っているのですが、これは通信事業社的な L3 のネットワークなんですよね。物理トポロジと L3 トポロジがほぼ 1:1 に対応づけられるようなネットワークです。

一方、我々 SIer が普段目にするようなネットワークというと割と L2/VLAN が主体となるようなネットワークが多い。まだまだ Core-(Aggregation)-Access みたいな階層型で VLAN バリバリなネットワークがあります。そういったところで Batfish が使えるのかどうなのかを知りたい。これが今回の狙いです。Batfish で L2 トポロジが扱えるかどうか、それをどう検証してみたのかを紹介します。(もっとこういうのもやろうよ、みたいなツッコミがあったらコメントください。)

## 検証

### Batfish

[batfish/allinone 894ab1bf2bf0](https://hub.docker.com/layers/batfish/allinone/latest/images/sha256-894ab1bf2bf0337e2ca245e2e2355789110b79553bb2dcf1aea040cf98fcf88e) (2019/10/15) を使っています。

### 対象とするネットワーク

まずはごくシンプルな L2 ネットワークが扱えるかどうかを見てみる、ということで 1 スイッチ 2 セグメントの構成で 2 パターン用意してみます。(検証に使ったファイルは  [corestate55/batfish\-l2\-topology\-test](https://github.com/corestate55/batfish-test-topology) に置いてあります。)

ひとつめ ([sample1](https://github.com/corestate55/batfish-test-topology/tree/master/l2/sample1)) は、シンプルに異なる IP サブネットを使うふたつのセグメントがあるネットワーク。

![sample1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/8d9c89eb-1d10-ba00-9f56-79a03a547af6.png)

[スイッチ (switch1) のコンフィグ](https://github.com/corestate55/batfish-test-topology/blob/master/l2/sample1/configs/switch1-confg)は一応 Catalyst3750G 実機からとっていますが、実際に各ホストをつなげてネットワークを組むことはしていません。まあシンプルなネットワークだし多分コンフィグに大きなミスはない…はず……。ホストは  [Host files (json)](https://github.com/batfish/batfish/wiki/Packaging-snapshots-for-analysis#host-files) で用意しておきます。

もうひとつ ([sample2](https://github.com/corestate55/batfish-test-topology/tree/master/l2/sample2)) はこれにちょっとひねりを入れたイジワル構成です。

![sample2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/a74f8f47-5c3a-ba51-6700-61492a8ef729.png)


物理トポロジと VLAN 100 (青) の方は変えていません。VLAN 200 (緑) は「同じIPサブネットを使う異なる L2 セグメント」です。言い方を変えると (L3 の観点で) VLAN 200 は VLAN 100 のコピーになっています。これは L2 の設定情報を読み取れないと扱えません。

### Batfish Query

Batfish で調べたいことはいろいろあるのですが、今回の目的は「NW構成図を書くための情報を集める」なので、トポロジ情報すなわち  [edges query](https://pybatfish.readthedocs.io/en/latest/questions.html#pybatfish.question.bfq.edges) の結果に着目します。この後解説をしますが、もうひとつ　[L1 topology file](https://github.com/batfish/batfish/wiki/Packaging-snapshots-for-analysis#layer1-topology-file) の有無による変化についても調べます。

## ひとまずやってみる

Batfish container を起動します。All-in-one 版を使っているけど今回は Jupyter notebook ではなくターミナル上から試すので tcp/8888 は入れていません。

```
hagiwara@dev01:~$ sudo docker run -p 9997:9997 -p 9996:9996 batfish/allinone
```

### Sample1

対話的にクエリ実行していきます[^bfq]。

[^bfq]: pybatfish の使い方とかについては  [Batfish を使ってネットワーク構成を可視化してみよう \(3\) \- Qiita](https://qiita.com/corestate55/items/10673ef74c33a24a0389) 等を参照。

```python
(bf-venv) hagiwara@dev01:~/batfish/batfish-test-topology$ python
Python 3.7.3 (default, Oct  7 2019, 12:56:13)
[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
>>> from pybatfish.question import bfq
>>> load_questions()
Successfully loaded 63 questions from remote
>>> bf_init_snapshot('/home/hagiwara/batfish/batfish-test-topology/l2/sample1', name='sample1', overwrite=True)
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-21 14:52:38.699000+09:00 Parse network configs 0 / 1.
status: CHECKINGSTATUS
.... 2019-10-21 14:52:38.699000+09:00 Reading, unpacking, and deserializing files containing 'org.batfish.datamodel.GenericConfigObject' instances 9 / 9.
status: TERMINATEDNORMALLY
.... 2019-10-21 14:52:38.699000+09:00 Deserializing objects of type 'org.batfish.datamodel.Configuration' from files 9 / 9.
Default snapshot is now set to sample1
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 14:52:39.314000+09:00 Begin job.
'sample1'
>>>
```

[edges query](https://pybatfish.readthedocs.io/en/latest/questions.html#pybatfish.question.bfq.edges) には `layer1` か `layer3` かしかない (`layer2` というクエリはない) のでそれぞれ見てみましょう。……とはいえこの時点でもう怪しいですよね。なぜなら、L2 の構成を読もうとすると L1 の構成情報 (トポロジ) 必要なんですよ。どのポートにどのノードがつながっていて、どこのセグメント (VLAN) にマッピングされるかがわからないといけない。でも今のところそういう情報がないわけです。まあ何も考えずにやるとどうなるかを見てみましょう。

Layer3

```python
>>> ans = bfq.edges(edgeType='layer3')
>>> ans.answer().frame()
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: ASSIGNED
.... 2019-10-21 12:55:35.542000+09:00 Begin job.
status: TERMINATEDNORMALLY
.... 2019-10-21 12:55:35.542000+09:00 Begin job.
           Interface                IPs  Remote_Interface         Remote_IPs
0       host21[eth0]  ['192.168.2.101']      host23[eth0]  ['192.168.2.103']
1       host21[eth0]  ['192.168.2.101']      host24[eth0]  ['192.168.2.104']
2       host24[eth0]  ['192.168.2.104']      host23[eth0]  ['192.168.2.103']
3       host21[eth0]  ['192.168.2.101']      host22[eth0]  ['192.168.2.102']
4   switch1[Vlan100]    ['192.168.1.1']      host13[eth0]  ['192.168.1.103']
5       host14[eth0]  ['192.168.1.104']  switch1[Vlan100]    ['192.168.1.1']
6       host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
7       host12[eth0]  ['192.168.1.102']      host13[eth0]  ['192.168.1.103']
8       host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
9       host12[eth0]  ['192.168.1.102']  switch1[Vlan100]    ['192.168.1.1']
10      host14[eth0]  ['192.168.1.104']      host12[eth0]  ['192.168.1.102']
11      host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.1']
12      host14[eth0]  ['192.168.1.104']      host13[eth0]  ['192.168.1.103']
13      host12[eth0]  ['192.168.1.102']      host14[eth0]  ['192.168.1.104']
14      host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']
15      host12[eth0]  ['192.168.1.102']      host11[eth0]  ['192.168.1.101']
16      host13[eth0]  ['192.168.1.103']      host11[eth0]  ['192.168.1.101']
17      host13[eth0]  ['192.168.1.103']      host12[eth0]  ['192.168.1.102']
18      host14[eth0]  ['192.168.1.104']      host11[eth0]  ['192.168.1.101']
19      host13[eth0]  ['192.168.1.103']  switch1[Vlan100]    ['192.168.1.1']
20      host23[eth0]  ['192.168.2.103']      host21[eth0]  ['192.168.2.101']
21      host13[eth0]  ['192.168.1.103']      host14[eth0]  ['192.168.1.104']
22  switch1[Vlan100]    ['192.168.1.1']      host12[eth0]  ['192.168.1.102']
23      host23[eth0]  ['192.168.2.103']      host24[eth0]  ['192.168.2.104']
24  switch1[Vlan100]    ['192.168.1.1']      host11[eth0]  ['192.168.1.101']
25  switch1[Vlan100]    ['192.168.1.1']      host14[eth0]  ['192.168.1.104']
26      host22[eth0]  ['192.168.2.102']      host21[eth0]  ['192.168.2.101']
27      host23[eth0]  ['192.168.2.103']      host22[eth0]  ['192.168.2.102']
28      host24[eth0]  ['192.168.2.104']      host22[eth0]  ['192.168.2.102']
29      host22[eth0]  ['192.168.2.102']      host23[eth0]  ['192.168.2.103']
30      host22[eth0]  ['192.168.2.102']      host24[eth0]  ['192.168.2.104']
31      host24[eth0]  ['192.168.2.104']      host21[eth0]  ['192.168.2.101']
>>>
```

Layer1

```python
>>> ans = bfq.edges(edgeType='layer1')
>>> ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: CHECKINGSTATUS
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 12:56:04.308000+09:00 Begin job.
Empty DataFrame
Columns: [Interface, Remote_Interface]
Index: []
>>>
```

なんとなく予想はしていましたが、まず

* Layer3 トポロジとしては、同じセグメントのノードが全部フルメッシュにつながるような形で認識されている
* Layer1 トポロジは持っていない

ことがわかりました。Layer3 edges については図にするとこんな感じになります。

![sample1-link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/a8b94b3a-6e57-9ed3-65c8-7218322d7aa2.png)


* IP アドレスを持つノード同士の接続をみる
* リンクは片方向リンクとして扱われるので、ひとつの接続は 2 本( 2 方向) のリンクとして表示される
* N-nodes の完全グラフのリンク数は N(N-1)/2 だが、片方向リンクとして扱うのでリンク数 * 2 = N(N-1) になる。
  VLAN 100 では Vlan Interface も IP アドレスを持つので 5 nodes のフルメッシュ (5 * 4 = 20), VLAN200 は 4 nodes のフルメッシュ (4 * 3 = 12) : あわせて 32 edges



### Sample2

Sample1 では、セグメント内はフルメッシュになるものの、VLAN 100/200 は個別のセグメントとして見えています。これは何によるものでしょうか? Sample1 は L2 セグメントとごとに異なる IP サブネットを使っているという「お行儀の良い」ネットワークであるために、L2 設定をみて分けたのか IP アドレスだけを見て分けたのかがわかりません。なので、Sample2 : L3 観点では全く同じ構成でどのように区別されるのかを見てみましょう。

```python
(bf-venv) hagiwara@dev01:~/batfish/batfish-test-topology$ python            
Python 3.7.3 (default, Oct  7 2019, 12:56:13)                                  
[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
>>> from pybatfish.question import bfq
>>> load_questions()
Successfully loaded 63 questions from remote
>>>
>>> bf_init_snapshot('/home/hagiwara/batfish/batfish-test-topology/l2/sample2', name='sample2', overwrite=True)
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-21 13:22:38.103000+09:00 Parse network configs 0 / 1.
status: ASSIGNED
.... 2019-10-21 13:22:38.103000+09:00 Parse network configs 0 / 1.
status: ASSIGNED
.... 2019-10-21 13:22:38.103000+09:00 Deserializing objects of type 'org.batfish.datamodel.Configuration' from files 6 / 9.
status: TERMINATEDNORMALLY
.... 2019-10-21 13:22:38.103000+09:00 Deserializing objects of type 'org.batfish.datamodel.Configuration' from files 9 / 9.
Default snapshot is now set to sample2
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-21 13:22:39.018000+09:00 Begin job.
status: TERMINATEDNORMALLY
.... 2019-10-21 13:22:39.018000+09:00 Begin job.
'sample2'
>>>
```

Layer3 (長いので中間をカット)

```python
>>> ans = bfq.edges(edgeType='layer3')
>>> ans.answer().frame()
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 13:23:53.710000+09:00 Begin job.                               
           Interface                IPs  Remote_Interface         Remote_IPs   
0   switch1[Vlan200]    ['192.168.1.1']      host13[eth0]  ['192.168.1.103']   
1   switch1[Vlan200]    ['192.168.1.1']      host23[eth0]  ['192.168.1.103']   
2       host14[eth0]  ['192.168.1.104']  switch1[Vlan100]    ['192.168.1.1']   
3   switch1[Vlan200]    ['192.168.1.1']      host22[eth0]  ['192.168.1.102']   
4       host22[eth0]  ['192.168.1.102']      host23[eth0]  ['192.168.1.103']   
5       host23[eth0]  ['192.168.1.103']      host21[eth0]  ['192.168.1.101']   
6   switch1[Vlan200]    ['192.168.1.1']      host24[eth0]  ['192.168.1.104']   
7       host22[eth0]  ['192.168.1.102']      host21[eth0]  ['192.168.1.101']   
8       host12[eth0]  ['192.168.1.102']  switch1[Vlan100]    ['192.168.1.1']   
9       host21[eth0]  ['192.168.1.101']      host23[eth0]  ['192.168.1.103']   
(省略)
70      host12[eth0]  ['192.168.1.102']      host24[eth0]  ['192.168.1.104']   
71      host23[eth0]  ['192.168.1.103']      host14[eth0]  ['192.168.1.104']   
72      host24[eth0]  ['192.168.1.104']      host13[eth0]  ['192.168.1.103']   
73      host22[eth0]  ['192.168.1.102']  switch1[Vlan200]    ['192.168.1.1']   
74      host11[eth0]  ['192.168.1.101']      host24[eth0]  ['192.168.1.104']   
75      host14[eth0]  ['192.168.1.104']      host21[eth0]  ['192.168.1.101']   
76      host22[eth0]  ['192.168.1.102']      host14[eth0]  ['192.168.1.104']   
77      host23[eth0]  ['192.168.1.103']      host11[eth0]  ['192.168.1.101']   
78      host22[eth0]  ['192.168.1.102']      host11[eth0]  ['192.168.1.101']   
79      host23[eth0]  ['192.168.1.103']      host12[eth0]  ['192.168.1.102']   

[80 rows x 4 columns]
>>>
```

Layer1 については、Sample1 の時と同様、情報を持っていないので何も出ません。

さて Layer3 では 80 個の edge が表示されました。ひとつずつリンクを追いかければわかるんですが面倒なのでまずはざっと数で見てみましょう。

5-nodes フルメッシュ * 2 セグメント (VLAN 100/200) で認識される場合 (5 * 4) * 2 = 40 edges のはずです。80 edges 出てくる時点で VLAN 100/200 を分離して認識しているというのはなくなりました。上の実行結果でも、例えば最後の No.79 は host23 - host12 の edge になっているので、ふたつのセグメントが同一視されている、つまり IP アドレスだけで隣接関係 (セグメント) が判断されていることがわかります。

でも IP アドレスでひとつのセグメントだと認識しているとして、10-nodes フルメッシュだと 10 * 9 = 90 edges だから数が合わない。……答えは簡単で、同じ IP アドレスを持つホスト同士 (たとえば host11 - host21) はリンクを持たないのでその分が省かれます。同じ IP アドレスを持つペアが 5 組あって片方向リンクなので 5 * 2 = 10 edges 引くので、90 - 10 = 80 edges になります。

確認してみましょう。host11 を source とする edge を取り出してみると、host21 との edge がないことがわかります[^bfedge]。これが 10-nodes それぞれにあるので 8 * 10 = 80 edges ということですね。

[^bfedge]: host11 - host12/22 など同じ IP への edge が複数あるものは認識されます。自分と同じ IP のノードとは通信できないので接続 (edge) は認識しない、そうでないものは通信可能なので接続を持つ、という解釈かな。通信可能なノードの中に同じアドレスを持っているものがいる(重複したIPがある)というのはまた別問題。

```python

>>> ans = bfq.edges(edgeType='layer3')                                         
>>> df = ans.answer().frame()                                                  
>>> df.loc[list(map(lambda d: d.hostname=='host11', df.Interface.values))]     
       Interface                IPs  Remote_Interface         Remote_IPs       
11  host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.1']       
40  host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']       
42  host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']       
47  host11[eth0]  ['192.168.1.101']  switch1[Vlan200]    ['192.168.1.1']       
49  host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']       
56  host11[eth0]  ['192.168.1.101']      host22[eth0]  ['192.168.1.102']       
60  host11[eth0]  ['192.168.1.101']      host23[eth0]  ['192.168.1.103']       
74  host11[eth0]  ['192.168.1.101']      host24[eth0]  ['192.168.1.104']       
>>>
```

## L1 Topology 情報を追加する

### L1 topology file

すでに書いたとおり、L2 構成情報を判断するためには L1 topology の情報が必要になります。Batfish のドキュメントでは [L1 topology file](https://github.com/batfish/batfish/wiki/Packaging-snapshots-for-analysis#layer1-topology-file) というものを渡すと L1 topology 情報を指定できるとあります。ドキュメントにはこうあります。

> layer-3 adjacencies are computed by combining the supplied layer-1 adjacencies with layer-2 and layer-3 configuration to get a more accurate model. This is especially useful if IP addresses are reused across the network on interfaces that are not actually adjacent in practice.

「より正確なモデルを得るために提供されたレイヤ 1 接続情報をレイヤ 2 / レイヤ 3 構成情報と組み合わせることにより、レイヤ 3 接続が計算されます。これは、実際には隣接していないインタフェース上のネットワークで IP アドレスが再利用されているときに便利です」……期待できそうな説明です。 [L1 topology file のサンプル](https://github.com/batfish/batfish/blob/master/networks/example/example_layer1_topology.json)をみると、これもひとつのリンクを片方向ずつ 2 個の edge として表現するようです。

では試してみましょう。[Sample1/2 で物理トポロジは同じなので同じ topology file を使えます](https://github.com/corestate55/batfish-test-topology/blob/l1-topology/sample1/layer1_topology.json)。

### Sample1 (w/ L1 topology file)

layer1_topology.json がある場合、`bf_init_snapshot` したときに `Reading layer-1 topology` が表示されます。(ファイル配置については [l1\-topology branch](https://github.com/corestate55/batfish-test-topology/tree/l1-topology) を参照してください。)

```python
(bf-venv) hagiwara@dev01:~/batfish/batfish-test-topology$ python
Python 3.7.3 (default, Oct  7 2019, 12:56:13)
[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
>>> from pybatfish.question import bfq
>>> load_questions()
Successfully loaded 63 questions from remote
>>>
>>> bf_init_snapshot('/home/hagiwara/batfish/batfish-test-topology/sample1', name='sample1', overwrite=True)
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-21 14:34:42.353000+09:00 Parse configurations 6 / 8.
status: ASSIGNED
.... 2019-10-21 14:34:42.353000+09:00 Parse network configs 0 / 1.
status: ASSIGNED
.... 2019-10-21 14:34:42.353000+09:00 Reading layer-1 topology 1 / 1.
status: TERMINATEDNORMALLY
.... 2019-10-21 14:34:42.353000+09:00 Reading layer-1 topology 1 / 1.
Default snapshot is now set to sample1
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 14:34:43.480000+09:00 Begin job.
'sample1'
>>>
```

Layer3 の出力には変化がありませんでした。(32 edges)

Layer1 の情報は正しく認識できているようです。

```python
>>> ans = bfq.edges(edgeType='layer1')
>>> ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 14:36:45.103000+09:00 Reading layer-1 topology 1 / 1.
                        Interface               Remote_Interface
0                    host13[eth0]  switch1[GigabitEthernet1/0/3]
1                    host14[eth0]  switch1[GigabitEthernet1/0/4]
2                    host22[eth0]  switch1[GigabitEthernet1/0/6]
3                    host23[eth0]  switch1[GigabitEthernet1/0/7]
4   switch1[GigabitEthernet1/0/6]                   host22[eth0]
5                    host21[eth0]  switch1[GigabitEthernet1/0/5]
6                    host11[eth0]  switch1[GigabitEthernet1/0/1]
7                    host12[eth0]  switch1[GigabitEthernet1/0/2]
8                    host24[eth0]  switch1[GigabitEthernet1/0/8]
9   switch1[GigabitEthernet1/0/5]                   host21[eth0]
10  switch1[GigabitEthernet1/0/1]                   host11[eth0]
11  switch1[GigabitEthernet1/0/2]                   host12[eth0]
12  switch1[GigabitEthernet1/0/3]                   host13[eth0]
13  switch1[GigabitEthernet1/0/4]                   host14[eth0]
14  switch1[GigabitEthernet1/0/7]                   host23[eth0]
15  switch1[GigabitEthernet1/0/8]                   host24[eth0]
>>>
```

### Sample2 (w/ L1 topology file)

Sample1 のセグメント分離は IP だけでもできるので、L2 構成情報が使われているのかどうかを判断できません。そこを確認してみましょう。

Layer3

```python
>>> ans = bfq.edges(edgeType='layer3')
>>> ans.answer().frame()
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-21 14:44:46.406000+09:00 Begin job.
           Interface                IPs  Remote_Interface         Remote_IPs
0   switch1[Vlan100]    ['192.168.1.1']      host13[eth0]  ['192.168.1.103']
1   switch1[Vlan200]    ['192.168.1.1']      host23[eth0]  ['192.168.1.103']
2       host14[eth0]  ['192.168.1.104']  switch1[Vlan100]    ['192.168.1.1']
3   switch1[Vlan200]    ['192.168.1.1']      host22[eth0]  ['192.168.1.102']
4       host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
5       host12[eth0]  ['192.168.1.102']      host13[eth0]  ['192.168.1.103']
6       host22[eth0]  ['192.168.1.102']      host23[eth0]  ['192.168.1.103']
7       host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
8       host23[eth0]  ['192.168.1.103']      host21[eth0]  ['192.168.1.101']
9   switch1[Vlan200]    ['192.168.1.1']      host24[eth0]  ['192.168.1.104']
(省略)
30      host13[eth0]  ['192.168.1.103']  switch1[Vlan100]    ['192.168.1.1']
31  switch1[Vlan200]    ['192.168.1.1']      host21[eth0]  ['192.168.1.101']
32      host13[eth0]  ['192.168.1.103']      host14[eth0]  ['192.168.1.104']
33  switch1[Vlan100]    ['192.168.1.1']      host12[eth0]  ['192.168.1.102']
34      host21[eth0]  ['192.168.1.101']  switch1[Vlan200]    ['192.168.1.1']
35  switch1[Vlan100]    ['192.168.1.1']      host11[eth0]  ['192.168.1.101']
36      host24[eth0]  ['192.168.1.104']  switch1[Vlan200]    ['192.168.1.1']
37  switch1[Vlan100]    ['192.168.1.1']      host14[eth0]  ['192.168.1.104']
38      host23[eth0]  ['192.168.1.103']  switch1[Vlan200]    ['192.168.1.1']
39      host22[eth0]  ['192.168.1.102']  switch1[Vlan200]    ['192.168.1.1']
>>>
```

40 edges ということは 5-nodes フルメッシュ * 2 として判断されている! L1 topology 未指定時は IP だけを見て全部ひとつのセグメントと判断されていたものが、ちゃんと L1/L2 情報を加味してふたつのセグメントとして判断されているということでしょう。

確認してみましょう。

```python

>>> df = ans.answer().frame()
>>> df.loc[list(map(lambda d: d.hostname=='host11', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
4   host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
7   host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
15  host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.1']
21  host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']
>>>
```

L1 topology 情報がない場合は host11 を source とする edge に VLAN 200 のノードも混ざっていましたが今回は混ざっていません。同じ IP サブネットを使用しているセグメントをちゃんと別物として扱えています。

## まとめ

Batfish をつかって L2 構成情報 (L2 topology) を出せるかどうか調べてみました。まあ、`edges` に `layer2` っていうオプションがない時点で期待薄ではあるんですが……。

* Batfish は原則 L3 (IPアドレス) 情報に基づいて隣接関係を見ている
* L2スイッチ等を中継して同じ L2 segment にあるノードはフルメッシュ接続として表現される
* L1 topology 情報を追加で与えることで、L1/L2 情報を加味して L3 接続 (L3 隣接関係) が計算される
    * L3 インタフェースが直接接続されていないネットワークについては L1 topology file を与えてやらないと正確なトポロジ計算にならない

という動作になるようです。まだやれていないこと・気になるところは

* 複数の L2 スイッチで構成される L2 セグメントが正しく認識されるか
  * Trunk VLAN の取り扱い
* L1/L2 トポロジをどこからどう持ってくるか

かなあ。今回 L1 topology の情報は手で書いて与えているけど、Batfish 使う動機のひとつに、こうした構成情報をとるのが難しい・自動でとりたい、みたいな思いもあるわけで。実際の所 L1/L2 トポロジ情報を実働している環境から上手く取れないところがあったりするんだよねえ。手書きかあ……みたいな心境であります。いずれにせよ L3 接続としてはフルメッシュで出てきてしまうものを実態に合わせて扱おうとするとどうやっても L1/L2 接続情報がないといけない。うーむ。

## 参考

*  [Exploring Batfish with Cumulus – part one - Cumulus Networks engineering blog](https://cumulusnetworks.com/blog/exploring-batfish-with-cumulus-part-one/)
  * Cumulus をつかって L3-Fabric を構成し、そこで Batfish を使ったチェックを入れるという話になっています。Fabric を作るときに BGP Unnumbered を使用するので、IPアドレスだけでは隣接関係を判断させることができないため L1 topology file を追加する、という使い方をしています。