BatfishでNW障害時の影響分析を行う①

# 1. はじめに
前回の記事で、NW機器のリンク・筐体障害発生と断時間計測を自動化する例をご紹介しました。
[pyATS/GenieでNW障害テストを自動化する](https://qiita.com/kitara/items/43a93e479e0bbe86f6ed)

ラボ環境では何度でも自由に障害テスト出来ますが、本番環境リリース後はそうは行かないと思います。
また、新規セグメント構築やルーティング・ACL設定追加を積み重ねていく中で設定ミスが生じ、障害発生時の系切替やサイト切替で問題が発覚するケースもあると思います。

今回ご紹介するOSSのネットワークConfig解析ツール「Batfish」では、実機のConfigファイルをインプットとして、インターフェース、ルーティング(BGP、OSPF、EIGRP、Static等)、ACL設定等の解析を行い、ある機器間が通信可能かシミュレート出来ます。
さらに、Batfish上で疑似的にリンク・筐体障害を発生させ、障害時の疎通性や通信経路も確認可能です。

これ以降で、実際に障害時の影響分析を行う例をメモしておきます。


# 2. ネットワーク構成
詳細は割愛しますが、CML上に以下の環境を構築しました。

- `csr1000v-0`⇒`iosvl2-0,1`向け  
`csr1000v-0`で、`10.0.0.0/8`向けのStaticルーティングを設定。
ネクストホップはVRRP VIPとし、通常時はMasterの`iosvl2-0`を経由。

- `iosvl2-0,1`～`iosvl2-3,4`間
各VLAN IFで、シングルエリア構成でOSPFを有効化し、等コストマルチパスで冗長化。

- `alpine-0`⇒`iosvl2-3,4`向け
HSRPを使用し、通常時はActiveの`iosvl2-2`を経由。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/ac66c3d8-8b0b-2af7-519a-618f9f1386ba.png)


# 3. Batfishのセットアップ

### 3-1. Batfish Dockerイメージの取得、コンテナ起動
DockerエンジンをインストールしたLinux等を用意し、`docker pull`コマンドでBatfishのイメージを取得します。
Batfishサービス本体のみの[batfish/batfish](https://hub.docker.com/r/batfish/batfish/tags)と、クライアント環境(pybatfish、Jupyter Notebook)を含んだ[batfish/allinone](https://hub.docker.com/r/batfish/allinone)がありますが、今回は前者を使用しました。
バージョンは2022年3月時点の最新版`2021.12.13.1143`にしています。
続いて、`docker run`コマンドでコンテナを起動します。

```
$ sudo docker pull batfish/batfish:2021.12.13.1143
$ sudo docker run -d --name batfish -p 9997:9997 -p 9996:9996 batfish/batfish:2021.12.13.1143
```

### 3-2. pybatfishのインストール
BatfishのPythonベースのクライアント`pybatfish`を上記のコンテナホストもしくは別の端末にインストールします。
今回は、別の端末のPython仮想環境上(venv)にインストールしました。

```
(venv) $ python3 -m pip install --upgrade pip
(venv) $ python3 -m pip install --upgrade pybatfish
```

### 3-3. pybatfishの初期セットアップ
- Configの保管
クライアント側に以下のようなディレクトリを作成し、解析対象のConfig(iosvl2、nxos9000)を保管しておきます。

```
Config保管ディレクトリ名/
    ┗━ configs/
        ┣━ router1.cfg
        ┣━ router2.cfg
        ┗━ ...
```

- コネクションのセットアップ
バージョン`2021.11.04.1095`リリースの際、pybatfishとBatfishサービス間のコネクションセットアップは`Session()`を使うようドキュメント改訂されています。
[pybatfish > Interacting with the Batfish service > Sessions](https://pybatfish.readthedocs.io/en/latest/notebooks/interacting.html#Sessions)

   以前はBatfishサービスをリモートで起動する場合、コードの修正が必要でしたが、`Session()`の引数`host`でIPアドレスを指定可能になりました（デフォルトは`localhost`です）。
また、`batfish/batfish`コンテナは通常`TCP/9997,9996`を使用しますが、何らかの事情で別のポート番号を使用したり、複数のBatfishコンテナを同時起動したい場合は引数`port_v1`、`port_v2`で変更可能です。

```python:snippet1
import pandas as pd
from pybatfish.client.session import Session
from pybatfish.datamodel import *
from pybatfish.datamodel.answer import *
from pybatfish.datamodel.flow import *

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option("display.max_colwidth", None)

# bf = Session(host="localhost")
bf = Session(host="<Batfishサービスのアドレス>", port_v1=9997, port_v2=9996)
```

- ネットワーク、スナップショットの作成
ネットワークとは、各機器やサブネット情報を論理的にグルーピングしたものです。解析対象ネットワークが1種類の場合は、最初に1つ作成すれば問題ないと思います。今回は`test_network`を作成しています。
またスナップショットとは、Configファイルから解析したネットワーク状態をファイル群としてまとめたものです。今回はまず、正常時のConfigファイルからスナップショット`test_snapshot`を作成しています。
ディレクトリは上記のConfig保管ディレクトリを指定します。

```python:snippet2
NETWORK_NAME = "test_network"
BASE_SNAPSHOT_NAME = "test_snapshot"
SNAPSHOT_DIR = '<Config保管ディレクトリ>'

# Create a network
bf.set_network(NETWORK_NAME)
# Initializing a new snapshot
bf.init_snapshot(SNAPSHOT_DIR, name=BASE_SNAPSHOT_NAME, overwrite=True)
```

- 認識出来ないコマンドの確認
Batfishは様々なベンダーのConfigをサポートしていますが、認識出来ないコマンドもあります。
解析結果が想定と異なる場合、重要な手掛かりになりますので、ここで該当のコマンド一覧を取得しておきます。

```python:snippet3
bf.q.initIssues().answer()
```

　 Jupyter Notebookで実行した結果は以下の通りです。`icam monitor scale`や`copp profile strict`が認識できていないようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/b29441a0-6024-bf29-b8f2-fe4993503934.png)


# 4. 正常時の確認

### 4-1. VRRP確認
まずは正常時のVRRPステータスを確認してみます。pybatfishでQuestionを実行することで、Batfishサービスへ各種問合せが可能です。VRRPの場合`vrrpProperties()`を使用します。

```python:snippet4
bf.q.vrrpProperties().answer().frame()
```
想定通りConfig解析が行われ、`iosvl2-0`がMaster、`iosvl2-1`がBackupとなっている事が分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/35f58548-32dd-1f38-b9d2-eedfadedb4c0.png)
(参考) `iosvl2-0`のVlan100設定
```
interface Vlan100
 ip address 10.1.1.253 255.255.255.0
 ip access-group acl_test in
 vrrp 100 ip 10.1.1.254
 vrrp 100 priority 110
```


### 4-2. ルーティング確認
ルーティングでは`routes()`を使用します。引数を指定しない場合、全機器の情報が表示されます。以下の例は`nodes="csr1000v-0"`で1台に絞っています。
```python:snippet5
bf.q.routes(nodes="csr1000v-0").answer().frame()
```
想定通りStatic、直接接続、ローカルのルーティング情報が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/85f5d7dd-7a98-545d-8921-317373869e08.png)

別の例として、全ルーティング情報を取得した後「宛先が`10.4.1.0/24`かつプロトコルが`ospf`」のルートのみフィルタすることも可能です。
```python:snippet6
routes_df = bf.q.routes().answer().frame()
routes_df[(routes_df['Network'] == "10.4.1.0/24") & (routes_df["Protocol"] == "ospf")]
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/14a1687a-25c8-9081-0fa1-95cbdea901fc.png)

### 4-3. OSPF確認
OSPFのネイバー関係は`ospfSessionCompatibility()`で確認可能です。こちらもデフォルトでは全機器の情報が表示されますので、`.head(5)`で最初の5つだけ表示しています。

```python:snippet7
bf.q.ospfSessionCompatibility().answer().frame().head(5)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/37635ef0-f6a0-4fef-45d2-4ddfda9428bc.png)

### 4-4. ACL設定確認
上記のようなステータス確認ではなく、Config設定も確認できます。例えばACL設定の場合、`definedStructures()`で`types="extended ipv4 access-list line"`を指定すれば「IPv4拡張ACL」を表示可能です。

```python:snippet8
bf.q.definedStructures(types="extended ipv4 access-list line").answer().frame()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/d8dd2aab-2fed-38a2-1392-f3f33c3ed56b.png)


### 4-5. 通信経路確認
最後に、ネットワーク構成図のPing送信元`10.1.1.100`から`10.4.1.100`宛ての疎通性および通信経路を確認してみます。
下記参考の通りいくつか選択肢がありますが、今回は`bidirectionalTraceroute()`を使ってみます。
引数として`startLocation='@enter(<開始ノード名>[<IF名>])'`、`headers=HeaderConstraints(dstIps='<宛先アドレス>', srcIps='<送信元アドレス>'))`を指定します。
bidirectionalと名が付いている通り、双方向の結果を返してくれます。以下の例では、`Forward_Traces[0][0]`で行きの経路の1つを表示しています。


```python:snippet9
result_bdtr = bf.q.bidirectionalTraceroute(
    startLocation='@enter(csr1000v-0[GigabitEthernet2])',
    headers=HeaderConstraints(dstIps='10.4.1.100', srcIps='10.1.1.100')).answer().frame()
result_bdtr.Forward_Traces[0][0]
```
1.`csr1000v-0`⇒2.`iosvl2-0`(VRRP Master)⇒3.`nxos9000-0`⇒4.`iosvl2-2`の経路で通信できている事が分かります。また、`RECEIVED`、`FORWARDED`、`TRANSMITTED`でそれぞれ受信、フォワーディング、送信の流れも確認できます。2.で`PERMITTED`と記載されている通り、ACL許可されている事も分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/ffa7dedf-76fe-511a-ea28-75f6a2db67cf.png)

また以下の例では、`Reverse_Traces[0][0]`で戻りの経路の1つを表示しています。
```python:snippet10
result_bdtr.Reverse_Traces[0][0]
```
行きの場合、宛先の`10.4.1.100`を持つ機器が存在しないため結果が`DELIVERED_TO_SUBNET`となっていますが、戻りの場合`csr1000v-0`が`10.1.1.100`を持っているため`ACCEPTED`となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/3dec1b52-7e85-88aa-8b6e-f3c593d18fba.png)

#### (参考) 通信経路確認で用いるQuestion
`traceroute`と`reachablity`の違いがややこしいのですが、前者は「Tracerouteのように、ある特定の送信元・宛先ホストの組み合わせが許可/拒否されるか確認する」用途なのに対し、後者は「ある送信元・宛先セグメントの組み合わせがすべて許可されるか、あるいは1つでも拒否される組み合わせがあるか検索する」用途のイメージです。
| No. | Question           | 内容                  |
|:---:|:-----------------|:------------------|
| 1  | traceroute (※)         | 指定した開始ノードから宛先へ仮想的なTracerouteを行う。実際のTracerouteと異なり、各ホップの戻り通信がNGでもIPアドレスは返してくれる |
| 2  | bidirectionalTraceroute (※)         | No.1に加え、戻りの確認も同時に行う |
| 3  | reachability (※)         | 指定した経路(開始/終了/経由箇所)、送信元/宛先情報(プロトコル、アドレス、ポート番号等)にマッチするフローを検索し、フローの例を出力 |
| 4  | bidirectionalReachability          | No.3に加え、戻りの確認も同時に行う |
| 5   | differentialReachability          | ある構成(現状)では許可されるが、別の構成(設定後)ではドロップされるフローを検出 |

(※) 引数`ignoreFilters=True`を指定することでACL設定を無視した経路確認が可能。


# 5. 最後に
ここまでで、Batfishを使って正常時の確認を行いました。別の記事で障害時の確認と、正常時・障害時を比較した影響分析も行ってみたいと思います。