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

## はじめに

前回: [BatfishでL2 Topologyを出せるかどうか調べてみる](https://qiita.com/corestate55/items/50ba0ae3e204d84fb03e) の補足的な記事です。前回は最も基本的な構成を、ということで 1-switch, 2-segment で試してみました。今回はそれをちょっと拡張して 2-switch, 2-segment です。以下の要素を足します。

* Trunk port
* Link Aggregation (Port channel)
* HSRP

## 構成

使用している Batfish container は前回と同じです。

* config 等は引き続き  [GitHub に置いてあります](https://github.com/corestate55/batfish-test-topology)。
  * 今回スイッチの増設は単純に config copy でやっているので間違えてるところがあるかもしれない……おかしいところを見つけたら教えてください。
*  [Layer1 topology file](https://github.com/corestate55/batfish-test-topology/blob/master/l2/sample3/layer1_topology.json) が必要なのは前回わかっているので最初から置きます。以下の Sample 3-5 ですべて同じ L1 topology です。

### Sample3

Sample1 の構成を 2-switch に変更しています。VLAN100 で HSRP, スイッチ間リンクに Port Channel, VLAN Trunk を使います。

![sample3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/016c2dd8-9d52-fe9d-c84a-c7bc8f734583.png)


### Sample4

Sample2 の構成を 2-switch に変更しています。追加点は sample3 と同様。同じ IP サブネットを使っている点に注意してください。

![sample4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/aaf2e38c-bb2e-deca-2272-1625e6ae0ffa.png)


### Sample5

Sample4 をちょっとイジワルな形にしているものです (異なる VLAN ID を使ってひとつの L2 segment をつくる)。まあ、通常こういう造りにあえてしようとは思わないでしょうけど、いちおう造りとしてはやれるので。

* 同一 IP サブネットを使用 (L3/L2 の構造自体は sample4 と同様)
* 同一 L2 セグメントを異なる VLAN ID で接続
  * このためスイッチ間リンクは trunk ではなく access です。

これは Batfish が複数スイッチにまたがる L2 domain をどのくらいちゃんと見ているのかというのを確認したくて設定しています。(環境内ですべての VLAN ID が一意に同じセグメントを指しているみたいな仮定を持ってしまうと読めない構成です。)

![sample5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/7725bbc5-5b4c-d76c-748d-0d838b246e90.png)

## 検証

### Sample3: 複数スイッチでの L2 Segment

まず Layer1 topology がちゃんと渡せているかを確認。物理リンクは 8-hosts + 2-interlinks なので片方向ずつ合計 20 本になりますが、スイッチ間リンクは物理インタフェースじゃなくて LAG (Port channel) で扱われているため (8 + 1) * 2 = 18 edges になっています。

```python

>>> ans = bfq.edges(edgeType='layer1')
>>> ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-28 11:41:18.702000+09:00 Reading layer-1 topology 1 / 1.
status: TERMINATEDNORMALLY
.... 2019-10-28 11:41:18.702000+09:00 Reading layer-1 topology 1 / 1.
                        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                    host23[eth0]  switch2[GigabitEthernet1/0/7]
5   switch2[GigabitEthernet1/0/8]                   host24[eth0]
6   switch1[GigabitEthernet1/0/6]                   host22[eth0]
7                    host21[eth0]  switch1[GigabitEthernet1/0/5]
8   switch2[GigabitEthernet1/0/7]                   host23[eth0]
9                    host11[eth0]  switch1[GigabitEthernet1/0/1]
10                   host12[eth0]  switch1[GigabitEthernet1/0/2]
11                   host24[eth0]  switch2[GigabitEthernet1/0/8]
12  switch1[GigabitEthernet1/0/5]                   host21[eth0]
13         switch2[Port-Channel1]         switch1[Port-Channel1]
14         switch1[Port-Channel1]         switch2[Port-Channel1]
15  switch2[GigabitEthernet1/0/3]                   host13[eth0]
16  switch1[GigabitEthernet1/0/1]                   host11[eth0]
17  switch1[GigabitEthernet1/0/2]                   host12[eth0]
>>>
```
つづいて L3 topology を見てみます。

```python

>>> ans = bfq.edges(edgeType='layer3')
>>> ans.answer().frame()                                                       
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
.... 2019-10-28 11:45:57.383000+09:00 Begin job.                               
status: TERMINATEDNORMALLY
.... 2019-10-28 11:45:57.383000+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   switch1[Vlan100]    ['192.168.1.2']  switch2[Vlan100]    ['192.168.1.3']   
4       host21[eth0]  ['192.168.2.101']      host22[eth0]  ['192.168.2.102']   
5   switch1[Vlan100]    ['192.168.1.2']      host13[eth0]  ['192.168.1.103']   
# 省略
37      host24[eth0]  ['192.168.2.104']      host22[eth0]  ['192.168.2.102']   
38  switch2[Vlan100]    ['192.168.1.3']  switch1[Vlan100]    ['192.168.1.2']   
39      host22[eth0]  ['192.168.2.102']      host23[eth0]  ['192.168.2.103']   
40      host22[eth0]  ['192.168.2.102']      host24[eth0]  ['192.168.2.104']   
41      host24[eth0]  ['192.168.2.104']      host21[eth0]  ['192.168.2.101']   
>>>
```

VLAN 100 が 4-hosts + 2-SVI なので 6 * 5 = 30 edges, VLAN 200 が 4-hosts なので 4 * 3 = 12 edges, 合わせて 42 edges ということですね。HSRP (VIP) は edges の計算には含まれないみたいです。

```python

>>> df = ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 11:49:34.377000+09:00 Begin job.
>>> df.loc[list(map(lambda d: d.hostname=='host11', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
7   host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
10  host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
14  host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.2']
17  host11[eth0]  ['192.168.1.101']  switch2[Vlan100]    ['192.168.1.3']
18  host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']
>>>
```


### (補足) Batfish 機能確認

Port-channel: 一部省略していますがちゃんと channel として property がとれています。

```python

>>> ifprops = bfq.interfaceProperties(nodes="switch1", interfaces="Port-channel1", properties='.*').answer()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 12:12:20.374000+09:00 Begin job.
>>> pprint.pprint(ifprops.rows)
[{'Access_VLAN': None,
...
  'Allowed_VLANs': '100,200',
  'Auto_State_VLAN': True,
  'Bandwidth': 2000000000.0,
  'Blacklisted': False,
  'Channel_Group': None,
  'Channel_Group_Members': ['GigabitEthernet1/0/23', 'GigabitEthernet1/0/24'],
  'DHCP_Relay_Addresses': [],
  'Declared_Names': ['Port-channel1'],
  'Description': None,
  'Encapsulation_VLAN': None,
...
  'Interface': {'hostname': 'switch1', 'interface': 'Port-Channel1'},
...
  'Switchport': True,
  'Switchport_Mode': 'TRUNK',
  'Switchport_Trunk_Encapsulation': 'DOT1Q',
...
}]
>>>

```

HSRP: いまの batfish で HSRP 情報を扱うようなクエリがない……? まず [ip owners query](https://pybatfish.readthedocs.io/en/latest/questions.html#pybatfish.question.bfq.ipOwners) で VIP が出てこない。[^ipowners]

[^ipowners]: Issueあった (2019/10/28時点では Open) : [IPOwners should include HSRP IPs · Issue \#4593 · batfish/batfish](https://github.com/batfish/batfish/issues/4593)

```python

>>> bfq.ipOwners().answer()
status: TRYINGTOASSIGN
.... no task information
status: ASSIGNED
status: TERMINATEDNORMALLY
.... 2019-10-28 12:21:41.246000+09:00 Begin job.
      Node      VRF Interface             IP Mask Active
0   host14  default      eth0  192.168.1.104   24   True
1   host13  default      eth0  192.168.1.103   24   True
2   host11  default      eth0  192.168.1.101   24   True
3   host22  default      eth0  192.168.2.102   24   True
4   host21  default      eth0  192.168.2.101   24   True
5   host24  default      eth0  192.168.2.104   24   True
6  switch1  default   Vlan100    192.168.1.2   24   True
7  switch2  default   Vlan100    192.168.1.3   24   True
8   host12  default      eth0  192.168.1.102   24   True
9   host23  default      eth0  192.168.2.103   24   True
>>>
```

[interface properties query](https://pybatfish.readthedocs.io/en/latest/questions.html#pybatfish.question.bfq.interfaceProperties) で確認してみると、HSRP Group は取れているけど VIP の情報が入ってきてないんだよねえ。

```python

>>> ifprops = bfq.interfaceProperties(nodes="switch1", interfaces="Vlan100", properties='.*').answer()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 12:17:32.803000+09:00 Begin job.
>>> pprint.pprint(ifprops.rows)
[{
...
  'All_Prefixes': ['192.168.1.2/24'],
...
  'Declared_Names': ['Vlan100'],
...
  'HSRP_Groups': ['100'],
  'HSRP_Version': None,
  'Incoming_Filter_Name': None,
  'Interface': {'hostname': 'switch1', 'interface': 'Vlan100'},
...
  'Primary_Address': '192.168.1.2/24',
  'Primary_Network': '192.168.1.0/24',
...
  'VRRP_Groups': [],
  'Zone_Name': None}]
>>>
```

### Sample4: 複数スイッチでの IP Subnet 重複

Layer1 topology は同様に出るので Layer3 topology を確認。VLAN 100/200 それぞれ 4-hosts + 2-SVI なので 6 * 5 * 2 = 60 edges になります。

```python

>>> ans = bfq.edges(edgeType='layer3')                                         
>>> ans.answer().frame()
status: BLOCKED
.... no task information
status: BLOCKED
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 12:40:30.090000+09:00 Begin job.                               
           Interface                IPs  Remote_Interface         Remote_IPs   
0   switch1[Vlan100]    ['192.168.1.2']  switch2[Vlan100]    ['192.168.1.3']   
1   switch1[Vlan200]    ['192.168.1.2']  switch2[Vlan200]    ['192.168.1.3']   
2   switch2[Vlan200]    ['192.168.1.3']  switch1[Vlan200]    ['192.168.1.2']   
3   switch1[Vlan200]    ['192.168.1.2']      host23[eth0]  ['192.168.1.103']   
4   switch2[Vlan200]    ['192.168.1.3']      host22[eth0]  ['192.168.1.102']   
# 省略
55  switch2[Vlan100]    ['192.168.1.3']  switch1[Vlan100]    ['192.168.1.2']   
56      host23[eth0]  ['192.168.1.103']  switch1[Vlan200]    ['192.168.1.2']   
57      host22[eth0]  ['192.168.1.102']  switch1[Vlan200]    ['192.168.1.2']   
58      host23[eth0]  ['192.168.1.103']  switch2[Vlan200]    ['192.168.1.3']   
59      host22[eth0]  ['192.168.1.102']  switch2[Vlan200]    ['192.168.1.3']   
>>>
```

各セグメントのノードをピックアップして確認。おなじ IP サブネットを使用していますがちゃんと L2 segment で分割できています。

```python

>>> df = ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 12:41:49.258000+09:00 Begin job.
>>> df.loc[list(map(lambda d: d.hostname=='host11', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
18  host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.2']
21  host11[eth0]  ['192.168.1.101']  switch2[Vlan100]    ['192.168.1.3']
39  host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
41  host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
45  host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']
>>> df.loc[list(map(lambda d: d.hostname=='host21', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
13  host21[eth0]  ['192.168.1.101']      host23[eth0]  ['192.168.1.103']
47  host21[eth0]  ['192.168.1.101']      host24[eth0]  ['192.168.1.104']
48  host21[eth0]  ['192.168.1.101']      host22[eth0]  ['192.168.1.102']
51  host21[eth0]  ['192.168.1.101']  switch1[Vlan200]    ['192.168.1.2']
52  host21[eth0]  ['192.168.1.101']  switch2[Vlan200]    ['192.168.1.3']
>>>
```

### Sample5: 同一 L2 segment 異 VLAN ID

L1 topology データとしては同じなのですが sample5 では port channel を使用していないので確認してみます。

```python

>>> ans = bfq.edges(edgeType='layer1')
>>> ans.answer().frame()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 12:45:30.873000+09:00 Reading layer-1 topology 1 / 1.
                         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                     host23[eth0]   switch2[GigabitEthernet1/0/7]
5   switch2[GigabitEthernet1/0/23]  switch1[GigabitEthernet1/0/23]
6    switch2[GigabitEthernet1/0/8]                    host24[eth0]
7    switch1[GigabitEthernet1/0/6]                    host22[eth0]
8                     host21[eth0]   switch1[GigabitEthernet1/0/5]
9   switch2[GigabitEthernet1/0/24]  switch1[GigabitEthernet1/0/24]
10   switch2[GigabitEthernet1/0/7]                    host23[eth0]
11  switch1[GigabitEthernet1/0/23]  switch2[GigabitEthernet1/0/23]
12  switch1[GigabitEthernet1/0/24]  switch2[GigabitEthernet1/0/24]
13                    host11[eth0]   switch1[GigabitEthernet1/0/1]
14                    host12[eth0]   switch1[GigabitEthernet1/0/2]
15                    host24[eth0]   switch2[GigabitEthernet1/0/8]
16   switch1[GigabitEthernet1/0/5]                    host21[eth0]
17   switch2[GigabitEthernet1/0/3]                    host13[eth0]
18   switch1[GigabitEthernet1/0/1]                    host11[eth0]
19   switch1[GigabitEthernet1/0/2]                    host12[eth0]
>>>
```

ちゃんとスイッチ間リンクが物理インタフェース Gi1/0/23-24 で表示されています。((8-hosts + 2-interlinks) * 2 = 20 edges)

L3 topology についても sample4 と同等になりました。異なる VLAN ID でひとつの L2 セグメントになっていることは、host11 の edge には switch1/VLAN100, switch2/VLAN200 が、host21 の edge には switch1/VLAN200, switch2/VLAN300 があることからも確認できます。

```python

>>> df.loc[list(map(lambda d: d.hostname=='host11', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
17  host11[eth0]  ['192.168.1.101']  switch1[Vlan100]    ['192.168.1.2']
38  host11[eth0]  ['192.168.1.101']      host12[eth0]  ['192.168.1.102']
40  host11[eth0]  ['192.168.1.101']      host13[eth0]  ['192.168.1.103']
46  host11[eth0]  ['192.168.1.101']      host14[eth0]  ['192.168.1.104']
47  host11[eth0]  ['192.168.1.101']  switch2[Vlan200]    ['192.168.1.3']
>>> df.loc[list(map(lambda d: d.hostname=='host21', df.Interface.values))]
       Interface                IPs  Remote_Interface         Remote_IPs
12  host21[eth0]  ['192.168.1.101']      host23[eth0]  ['192.168.1.103']
22  host21[eth0]  ['192.168.1.101']  switch2[Vlan300]    ['192.168.1.3']
49  host21[eth0]  ['192.168.1.101']      host24[eth0]  ['192.168.1.104']
50  host21[eth0]  ['192.168.1.101']      host22[eth0]  ['192.168.1.102']
54  host21[eth0]  ['192.168.1.101']  switch1[Vlan200]    ['192.168.1.2']
>>>
```

## まとめ

[前回](https://qiita.com/corestate55/items/50ba0ae3e204d84fb03e) 1-switch 2-segment で確認したモノを 2-switch に拡張してみました。Trunk/Access や LAG (Port channel) についてもちゃんと扱えていますね。主要な L2 (topology) 設定についてはおおむね parse できているようです。

`edges` ではあまり問題にはならないですが、HSRP (VRRP) についてはどういう扱いになっているのかもうちょっと確認した方が良さそうですね……。

### L2 Topology をどう出すか

L2 Topology: L2 segment がどのスイッチのどのポートを通ってどのノードと接続されているか、というのはどう求めれば良いのでしょうか?

まず、前提として

* NW 機器コンフィグは与えられる
* L1 topology はどうにかして取得できる
  * L1 config がないと L2 topology の計算ができないので。とはいえ L1 topology をちゃんと作るのもそれはそれで面倒なんですが

とします。そのうえで各 L2 segment 間の接続を見ていくのがベタだけどひとつの方法でしょう。 [Switched VLAN properties query](https://pybatfish.readthedocs.io/en/latest/questions.html#pybatfish.question.bfq.switchedVlanProperties) では VLAN がどのインタフェースにマッピングされているかを求めることができます。(下記は sample4 での実行例)

```python

>>> svprops = bfq.switchedVlanProperties(nodes="switch1").answer()
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2019-10-28 14:15:33.307000+09:00 Begin job.
>>> pprint.pprint(svprops.rows)
[{'Interfaces': [{'hostname': 'switch1', 'interface': 'Vlan1'}],
  'Node': {'id': 'node-switch1', 'name': 'switch1'},
  'VLAN_ID': 1,
  'VXLAN_VNI': None},
{'Interfaces': [{'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/1'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/2'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/23'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/24'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/3'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/4'},
                 {'hostname': 'switch1', 'interface': 'Port-Channel1'},
                 {'hostname': 'switch1', 'interface': 'Vlan100'}],
  'Node': {'id': 'node-switch1', 'name': 'switch1'},
  'VLAN_ID': 100,
  'VXLAN_VNI': None},
{'Interfaces': [{'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/23'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/24'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/5'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/6'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/7'},
                 {'hostname': 'switch1', 'interface': 'GigabitEthernet1/0/8'},
                 {'hostname': 'switch1', 'interface': 'Port-Channel1'},
                 {'hostname': 'switch1', 'interface': 'Vlan200'}],
  'Node': {'id': 'node-switch1', 'name': 'switch1'},
  'VLAN_ID': 200,
  'VXLAN_VNI': None}]
>>>
```

これらの情報を元に、各スイッチ/セグメント (VLAN) ごとにポート対応、隣接スイッチとの接続情報対応をみて L2 segment の範囲とかを洗い出せばよいはず。

ただ、これまで見てきたように Batfish は L3 node (IP addr) 間の隣接関係を L1/L2 情報に基づいて計算できています。つまり、こうした L2 トポロジ情報を内部的にはもっているはずなんですよね。でもそれをひっぱり出す口がない。あるスイッチの VLAN/Port の対応、各ポートについて対向のポートの VLAN 設定を突き合わせていって L2 トポロジを作るって実装はまあできるとは思うし、手動でやるよりはやりやすくはなっているでしょう。だけど、batfish の内部で恐らくやってるであろう処理を外で作り直すであろうことと、そもそもそういった面倒な計算をお任せできるツールとして期待しているというのがあって、なんかもうちょっといい方法ないのかなあとぐるぐる考えてしまいますね……。