Genie DiffによるCisco機器のConfig/showコマンド比較

# はじめに
Ciscoが公開しているPythonベースのテストフレームワーク/フィーチャーライブラリー「pyATS/Genie」の内、Genie Diffを使ってみた結果をメモしておきます。

# Genie Diffとは
2つの辞書データもしくはオブジェクトを比較し、差分（追加・削除・修正）を出力してくれるPythonオブジェクトです。
Configやshowコマンドの結果をそのまま比較するのではなく、例えばConfigはGenie Config、showコマンドはGenie Parserを使って辞書形式の構造化データに変換したり、Genie ConfやGenie Opsのオブジェクト形式にしたりする必要があります。

参考URL： [Genie Utils - Diff](https://pubhub.devnetcloud.com/media/pyats-packages/docs/genie/utils/index.html#diff)

以下の1.でConfig、2.でshowコマンドの比較例をご紹介します。


# 1. Configの比較
## 1-1. Configデータの構造化
Python環境のセットアップは[こちら](https://qiita.com/tech_kitara/items/cf39ecf0ae409235bf5d#%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97)の記事を参照願います。(Testbedも用意する必要あり。)
今回はCisco CSR1000Vのshow runを取得し、辞書形式の構造化データに変換します。

## 1-2. Pythonコード①
`execute`メソッドでshow runを実行した後、`Config`オブジェクトから`config`インスタンスを生成し、`tree`メソッドで構造化しています。

```python:genie9-1.py
from genie.conf import Genie
from genie.utils.config import Config
from pprint import pprint

testbed = Genie.init('testbed_devnet3.yaml')
device = testbed.devices['csr1000v-1']

device.connect()

# show runを実行
cfg = device.execute('show run')
print(cfg)

# show runの結果を辞書形式の構造化データに変換
config = Config(cfg)
config.tree()
pprint(config.config)
```

## 1-3. 実行結果①
以下は構造化前のshow run結果の抜粋です。

```yaml:
$ python genie9-1.py
# 途中の実行ログは省略

# print(cfg)の結果抜粋
Building configuration...

Current configuration : 6818 bytes
!
! Last configuration change at 04:47:24 UTC Sat Jul 13 2019 by developer
!
version 16.11
service timestamps debug datetime msec
service timestamps log datetime msec
(中略)
interface GigabitEthernet1
 description MANAGEMENT INTERFACE - DON'T TOUCH ME
 ip address 10.10.20.48 255.255.255.0
 negotiation auto
 no mop enabled
 no mop sysid
!
interface GigabitEthernet2
 description Network Interface
 no ip address
 shutdown
 negotiation auto
 no mop enabled
 no mop sysid
!
interface GigabitEthernet3
 description Network Interface
 no ip address
 shutdown
 negotiation auto
 no mop enabled
 no mop sysid
!
(中略)
```

以下は構造化後のデータです。上記に対応する部分のみ抜き出しています。
階層構造を持つinterfaceコマンドは、ネストの辞書データに変換されています。また、全体的に順番が大文字A-Z⇒小文字A-Zに変更されています。元データ内の「! Last configuration ～」は、構造化データでは除外されていました。

```yaml:
# pprint(config.config)の結果抜粋
{'Building configuration...': {},
 'Current configuration : 6818 bytes': {},
 'interface GigabitEthernet1': {"description MANAGEMENT INTERFACE - DON'T TOUCH ME": {},
                                'ip address 10.10.20.48 255.255.255.0': {},
                                'negotiation auto': {},
                                'no mop enabled': {},
                                'no mop sysid': {}},
 'interface GigabitEthernet2': {'description Network Interface': {},
                                'negotiation auto': {},
                                'no ip address': {},
                                'no mop enabled': {},
                                'no mop sysid': {},
                                'shutdown': {}},
 'interface GigabitEthernet3': {'description Network Interface': {},
                                'negotiation auto': {},
                                'no ip address': {},
                                'no mop enabled': {},
                                'no mop sysid': {},
                                'shutdown': {}},
 'service timestamps debug datetime msec': {},
 'service timestamps log datetime msec': {},
 'version 16.11': {}}
```

## 1-4. 構造化データの差分比較
今回は、新規IFの設定を行い、その前後でConfig比較をしてみます。

## 1-5. Pythonコード②
大まかな流れは以下の通りです。
(1) 事前の構造化データを取得
(2) 新規IF設定
(3) 事後の構造化データを取得
(4) 結果を比較

```python:genie9-2.py
from genie.conf import Genie
from genie.utils.config import Config
from genie.conf.base import Interface
from genie.utils.diff import Diff

testbed = Genie.init('testbed_devnet3.yaml')
device = testbed.devices['csr1000v-1']

device.connect()

# (1) 事前の構造化データを取得
cfg1 = device.execute('show run')

config1 = Config(cfg1)
config1.tree()
cfg_dict1 = config1.config

# (2) 新規IF設定
# create an interface object
intf = Interface(device=device, name='GigabitEthernet2')

# configuration the interface by setting various object level attributes
intf.description = '< For Test >' 
intf.ipv4 = '10.1.1.1'
intf.ipv4.netmask = '255.255.255.0'
intf.shutdown = False

# apply configuration on device
intf.build_config()

# (3) 事後の構造化データを取得
cfg2 = device.execute('show run')

config2 = Config(cfg2)
config2.tree()
cfg_dict2 = config2.config

# (4) 結果を比較
dd = Diff(cfg_dict1, cfg_dict2)
dd.findDiff()
print(dd)
```

## 1-6. 実行結果②
`+`は2つ目のConfigで追加された設定、`-`は削除された設定です。
`interface GigabitEthernet2:`には何も付いていませんが、これは自身ではなく配下の設定で差異があったためです。

```yaml:
+Current configuration : 6793 bytes: 
-Current configuration : 6757 bytes: 
interface GigabitEthernet2:
+ description < For Test >: 
+ ip address 10.1.1.1 255.255.255.0: 
- no ip address: 
- shutdown: 
```

## 1-7. Pythonコード③（差分の一部のみ表示）
コード②ではすべての差分をまとめて出力しましたが、追加（add）、削除（remove）、修正（modified）を分けて出力したり、動的設定などを比較対象から除外したりすることも可能です。

#### (1) 追加だけ表示する場合
Diffオブジェクトの引数に、`mode='add'`を追加します。

```python:
dd = Diff(cfg_dict1, cfg_dict2, mode='add')
```

#### (2) 削除だけ表示する場合
Diffオブジェクトの引数に、`mode='remove'`を追加します。

```python:
dd = Diff(cfg_dict1, cfg_dict2, mode='remove')
```

#### (3) 修正だけ表示する場合
Diffオブジェクトの引数に、`mode='modified'`を追加します。

```python:
dd = Diff(cfg_dict1, cfg_dict2, mode='modified')
```

#### (4) 比較対象から除外する場合
除外対象のコマンドをリスト形式の変数`exclude_list`に格納し、Diffオブジェクトの引数で、`exclude=exclude_list`を指定します。

```python:
exclude_list = ['(^Using.*)', '(Building.*)', '(Current.*)', '(crypto pki certificate chain.*)']
dd = Diff(cfg_dict1, cfg_dict2, exclude=exclude_list)
```

## 1-8. 実行結果③（差分の一部のみ表示）

#### (1) 追加だけ表示する場合

```yaml:
+Current configuration : 6793 bytes: 
interface GigabitEthernet2:
+ description < For Test >: 
+ ip address 10.1.1.1 255.255.255.0: 
```

#### (2) 削除だけ表示する場合

```yaml:
-Current configuration : 6757 bytes: 
interface GigabitEthernet2:
- no ip address: 
- shutdown:
```

#### (3) 修正だけ表示する場合
今回は何も表示されません。修正の扱いになるのは、[2-3. 実行結果④](https://qiita.com/tech_kitara/items/4c3b2b842a6deb232be4#2-3-%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C)の`enabled`や`last_input`のように、`key: value`の内、`key`は変わらず`value`のみ変わっているものです。

#### (4) 比較対象から除外する場合
「Current configuration : ～」が除外されている事が分かります。

```yaml:
interface GigabitEthernet2:
+ description < For Test >: 
+ ip address 10.1.1.1 255.255.255.0: 
- no ip address: 
- shutdown: 
```

# 2. showコマンドの比較
## 2-1. showコマンドデータの構造化
showコマンド結果を辞書形式の構造化データに変換する方法は、以下記事の通りです。

[Genie Parser/OpsによるCisco機器のログ取得・構文解析](https://qiita.com/tech_kitara/items/f5c4ae6cbab8e7240f40)

## 2-2. Pythonコード④
大まかな流れはコード②と同じです。事前事後で、`show run`の代わりに`show interfaces GigabitEthernet2`を実行しています。

```python:genie9-3.py
from genie.conf import Genie
from genie.utils.config import Config
from genie.conf.base import Interface
from genie.utils.diff import Diff

testbed = Genie.init('testbed_devnet3.yaml')
device = testbed.devices['csr1000v-1']

device.connect()

# (1) 事前の構造化データを取得
output1 = device.parse('show interfaces GigabitEthernet2')

# (2) 新規IF設定
# create an interface object
intf = Interface(device=device, name='GigabitEthernet2')

# configuration the interface by setting various object level attributes
intf.description = '< For Test >' 
intf.ipv4 = '10.1.1.1'
intf.ipv4.netmask = '255.255.255.0'
intf.shutdown = False

# apply configuration on device
intf.build_config()

# (3) 事後の構造化データを取得
output2 = device.parse('show interfaces GigabitEthernet2')

# (4) 結果を比較
dd = Diff(output1, output2)
dd.findDiff()
print(dd)
```

## 2-3. 実行結果④
各種ステータスを差分比較出来ている事が分かります。

```yaml:
GigabitEthernet2:
 counters:
+  out_interface_resets: 16
-  out_interface_resets: 14
+ enabled: True
- enabled: False
+ last_input: 00:13:40
- last_input: 00:13:35
+ last_output: 00:04:36
- last_output: 00:04:31
+ description: < For Test >
+ ipv4: 
+  10.1.1.1/24: 
+   ip: 10.1.1.1
+   prefix_length: 24
```

# 補足
Genie Diffは、Ansibleのフィルタープラグインにも組み込まれています。（CiscoDevNetから公開されているもので、正式版にはマージされていません。）
[GitHub - ansible-pyats](https://github.com/CiscoDevNet/ansible-pyats#change-acl-configuration-and-compare-before-and-after-configs-using-genie_diff)
interfaceコマンドのような階層構造を理解して差分比較できる方法は、おそらく既存のAnsibleには存在しないため、Ansibleユーザーの方にとっても非常に有用な機能だと思います。