Skip to content

pybatfish で対象コンフィグが更新されている場合のみ、スナップショットを再作成する

batfish に同梱されているサンプルスクリプトや、インターネット上で見かけるスクリプト例は「スナップショットの有無に関わらず、スナップショットを生成する」ものが多いと思います ("例" なので、そういうものなのだと思います)。 仮に何度もスクリプトを実行し直す場合は、「コンフィグに変化が無いのであれば、毎回のスナップショット再作成は不要」という場合もあると思います。 そういった場合の実装例をメモしておきます。

例として Question には ipOwners を利用していますが、どの Question を利用してもスナップショット初期化部分の考え方は同じです。

前提条件

今回は Ubuntu 20.04LTS 上で、予め batfish を Docker で動作させています。 また、ディレクトリ構造は以下としています。 r1.cfgr2.cfg が batfish の解析対象となるネットワーク機器のコンフィグです。

1
2
3
4
5
├── example
│   └── configs
│       ├── r1.cfg
│       └── r2.cfg
└── ipOwners.py

基本的な使い方

pybatfish を使った基本的なサンプルは以下です。 batfish に同梱されたサンプルと、ほぼ同じ内容です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pybatfish.client.commands import *
from pybatfish.question import bfq
from pybatfish.question.question import load_questions

BASENAME = "example"
SNAPSHOT_PATH = "./" + BASENAME
SNAPSHOT_NAME = BASENAME + "_snapshot"
NETWORK_NAME = BASENAME + "_network"

bf_set_network(NETWORK_NAME)
bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)

load_questions()
result = bfq.ipOwners().answer().frame()
print(result.sort_values(by=["Node", "VRF", "Interface"]))

実行例は以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# ./ipOwners.py
Successfully loaded 68 questions from remote
Successfully loaded 68 questions from remote
status: TRYINGTOASSIGN
.... no task information
status: CHECKINGSTATUS
.... no task information
status: TERMINATEDNORMALLY
.... 2022-03-13 03:25:39.045000+00:00 Deserializing objects of type 'org.batfish.datamodel.Configuration' from files 2 / 2.
Default snapshot is now set to example_snapshot
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2022-03-13 03:25:39.341000+00:00 Parse environment BGP tables.
status: TRYINGTOASSIGN
.... no task information
status: TERMINATEDNORMALLY
.... 2022-03-13 03:25:39.473000+00:00 Begin job.
  Node      VRF           Interface        IP Mask Active
1   r1  default  GigabitEthernet0/0  10.0.0.1   24   True
0   r2  default  GigabitEthernet0/0  10.0.0.2   24   True

ロギングを無効化する

pybatfish のロギングを無効化し、結果だけを表示させるには logging の設定を行います。 下記の例では ERROR にしていますが、ロギングレベルは必要に応じて調整します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import logging

from pybatfish.client.commands import *
from pybatfish.question import bfq
from pybatfish.question.question import load_questions

logging.getLogger("pybatfish").setLevel(logging.ERROR)
BASENAME = "example"
SNAPSHOT_PATH = "./" + BASENAME
SNAPSHOT_NAME = BASENAME + "_snapshot"
NETWORK_NAME = BASENAME + "_network"

bf_set_network(NETWORK_NAME)
bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)

load_questions()
result = bfq.ipOwners().answer().frame()
print(result.sort_values(by=["Node", "VRF", "Interface"]))

実行例は以下の通りです。 ログ表示が省略され、結果だけ表示されていることが分かります。

1
2
3
4
# ./ipOwners.py
  Node      VRF           Interface        IP Mask Active
1   r1  default  GigabitEthernet0/0  10.0.0.1   24   True
0   r2  default  GigabitEthernet0/0  10.0.0.2   24   True

インデックスを表示しない

結果表示の際、インデックスを無効化するには Pandas の DataFrame を文字列へ変換して表示する際に .to_string(index=False) を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import logging

from pybatfish.client.commands import *
from pybatfish.question import bfq
from pybatfish.question.question import load_questions

logging.getLogger("pybatfish").setLevel(logging.ERROR)
BASENAME = "example"
SNAPSHOT_PATH = "./" + BASENAME
SNAPSHOT_NAME = BASENAME + "_snapshot"
NETWORK_NAME = BASENAME + "_network"

bf_set_network(NETWORK_NAME)
bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)

load_questions()
result = bfq.ipOwners().answer().frame()
print(result.sort_values(by=["Node", "VRF", "Interface"]).to_string(index=False))

実行結果は以下の通りです。 これまでのサンプルでは左側に表示されていたインデックスが表示されなくなりました。

1
2
3
4
# ./ipOwners.py
Node      VRF           Interface        IP Mask Active
  r1  default  GigabitEthernet0/0  10.0.0.1   24   True
  r2  default  GigabitEthernet0/0  10.0.0.2   24   True

必要時だけ初期化を実行する

スクリプトを呼び出す度にスナップショットを初期化すると毎回、実行時間が長くなってしまいます。 コンフィグ全体のハッシュ値を計算し、コンフィグに変更があった場合にのみスナップショットを更新 (再作成) するには、例えば以下のようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import hashlib
import logging
import os

from pybatfish.client.commands import *
from pybatfish.question import bfq
from pybatfish.question.question import load_questions


def get_dir_hash(directory: str) -> str:
    if not os.path.exists(directory):
        return -1
    hash = hashlib.sha1()
    for root, dirs, files in os.walk(directory):
        for file in files:
            with open(os.path.join(root, file), "rb") as f:
                while True:
                    buf = f.read(hash.block_size * 0x800)
                    if not buf:
                        break
                    hash.update(buf)
    return hash.hexdigest()


def read_hash(file: str) -> str:
    with open(file) as f:
        return f.read()


def write_hash(file: str, path: str):
    with open(file, mode="w") as f:
        f.write(get_dir_hash(path))


def init_bfq(base: str):
    BASENAME = base
    SNAPSHOT_PATH = "./" + BASENAME
    SNAPSHOT_NAME = BASENAME + "_snapshot"
    NETWORK_NAME = BASENAME + "_network"
    CONFIGS = SNAPSHOT_PATH + "/configs"
    HASH = SNAPSHOT_PATH + "/hash"
    bf_set_network(NETWORK_NAME)
    if os.path.exists(HASH):
        hash1 = read_hash(HASH)
        hash2 = get_dir_hash(CONFIGS)
        if hash1 == hash2:
            try:
                bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=False)
            except:
                bf_set_snapshot(name=SNAPSHOT_NAME)
        else:
            write_hash(HASH, CONFIGS)
            bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)
    else:
        write_hash(HASH, CONFIGS)
        bf_init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)
    load_questions()


logging.getLogger("pybatfish").setLevel(logging.ERROR)
BASENAME = "example"
init_bfq(BASENAME)
result = bfq.ipOwners().answer().frame()
print(result.sort_values(by=["Node", "VRF", "Interface"]).to_string(index=False))

実行結果は以下の通りです。 初回はやや時間がかかっていますが、2 回目以降は実行時間が短縮されていることが分かります。 また、途中でコンフィグを変更した場合は自動的にスナップショットが再作成されますが、その場合はやはり再計算分の時間がかかります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# time ./ipOwners.py
Node      VRF           Interface        IP Mask Active
  r1  default  GigabitEthernet0/0  10.0.0.1   24   True
  r2  default  GigabitEthernet0/0  10.0.0.2   24   True

real 0m1.579s
user 0m0.800s
sys 0m0.703s
# time ./ipOwners.py
Node      VRF           Interface        IP Mask Active
  r1  default  GigabitEthernet0/0  10.0.0.1   24   True
  r2  default  GigabitEthernet0/0  10.0.0.2   24   True

real 0m0.844s
user 0m0.719s
sys 0m0.735s
# time ./ipOwners.py
Node      VRF           Interface        IP Mask Active
  r1  default  GigabitEthernet0/0  10.0.0.1   24   True
  r2  default  GigabitEthernet0/0  10.0.0.2   24   True

real 0m0.842s
user 0m0.730s
sys 0m0.718s