はじめに
以前の記事で、Cisco IOS-XEのACL操作をRESTCONFで行いました。
RESTCONFでは、HTTP(S)のメッセージボディ部分をXMLやJSON形式で定義できます。
今回は、JSON形式で定義したACL設定と、同じく設定後にJSON形式で取得したACL設定を差分比較し、想定通り設定が反映されているか確認してみたいと思います。
1. 用意した環境
Cisco DevNet SandboxのIOS XE on CSR Recommended CodeのCSR1000v 16.9.3を利用させて頂きました。
Python3.6.8のvenv仮想環境上で、Ansible2.9.2をインストールして使いました。
2. フィルタープラグイン
ちょっと探した限り、Ansible公式でJSONデータを一発で差分比較出来そうなものがなかったため、DeepDiffをカスタムフィルタープラグインとして取り込みました。こちらはPythonベースのOSSで、辞書、リスト、文字列等のオブジェクトがネスト構造で組み合わさったデータを再帰的に(深く辿って)差分比較可能です。
最新バージョンではPython 2がサポートされませんので、利用時は注意が必要です。
2-1. 事前準備
DeepDiffのインストールを行います。
$ pip install deepdiff
2-2. Pythonコード(抜粋)
こちらの記事と同様、filter_plugins
ディレクトリ内に以下のファイルを格納しました。(実際には、他のカスタムフィルターも同ファイル内に書いています。)
やっている事は以下の3点です。
-
PY3
とHAS_DEEPDIFF
で、それぞれPython 3とDeepDiffがインストールされているか判定 -
DeepDiff(output1, output2)
で、2つのJSONデータを差分比較 - 出力結果
diff
をto_json()
でJSON形式に変換(Ansible側の出力を、インデントありの整形されたデータにするため)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.six import PY3, string_types
from ansible.errors import AnsibleError, AnsibleFilterError
try:
from deepdiff import DeepDiff # For Deep Difference of 2 objects
HAS_DEEPDIFF = True
except ImportError:
HAS_DEEPDIFF = False
class FilterModule(object):
def deepdiff(self, output1, output2):
if not PY3:
raise AnsibleFilterError("DeepDiff requires Python 3")
if not HAS_DEEPDIFF:
raise AnsibleFilterError("DeefDiff not found. Run 'pip install deepdiff'")
ddiff = DeepDiff(output1, output2)
return ddiff.to_json()
def filters(self):
return {
'deepdiff': self.deepdiff
}
3. Inventoryファイル
以前の記事と同じものを使いました。
4. 差分比較
これ以降で、ACL設定が無い状態から、名前付き拡張ACLTEST
×2行と、TEST2
×1行を作成し、事前事後で差分比較してみます。
ただし、一筋縄では行かなかったため、メモを兼ねて、トライ&エラーの過程を残しておきます。
4-1. 実行(1回目)
Playbookの大まかな流れは以下の通りです。
- 設定するACLをプレイ変数
content_data
で定義。name: TEST2
以降は、各要素の順番をバラバラに入れ替えています。また、宛先のワイルドカードマスクをあえて誤った設定dest-mask: 255.255.255.0
にしています(本来はdest-mask: 0.0.0.255
)。確認のため、access-list
配下の内容を表示。 -
restconf_config
モジュールを使い、JSON形式に変換したcontent_data
をHTTPSのボディに格納し、PATCH
メソッドでマージ。 -
restconf_get
モジュールを使い、設定されたACL情報をJSON形式で取得。 - タスク3の出力結果を表示。
-
deepdiff
フィルタープラグインを用い、タスク1の想定結果とタスク4の出力結果を差分比較。
---
- hosts: cisco
gather_facts: no
vars:
content_data:
Cisco-IOS-XE-native:ip:
access-list:
Cisco-IOS-XE-acl:extended:
- access-list-seq-rule:
- ace-rule:
action: permit
dest-ipv4-address: 192.168.100.0
dest-mask: 0.0.0.255
ipv4-address: 192.168.4.0
mask: 0.0.0.255
protocol: ip
sequence: 30
- ace-rule:
action: permit
dest-ipv4-address: 192.168.100.0
dest-mask: 0.0.0.255
ipv4-address: 192.168.5.0
mask: 0.0.0.255
protocol: ip
sequence: 50
name: TEST
- name: TEST2
access-list-seq-rule:
- sequence: 70
ace-rule:
action: permit
mask: 0.0.0.255
dest-ipv4-address: 192.168.100.0
dest-mask: 255.255.255.0
ipv4-address: 192.168.7.0
protocol: ip
tasks:
- name: display intended data # (1)
debug:
msg: "{{ content_data['Cisco-IOS-XE-native:ip']['access-list'] }}"
- name: create new acl # (2)
restconf_config:
path: /data/Cisco-IOS-XE-native:native/ip
method: patch
content: "{{ content_data | to_json }}"
- name: get acl info # (3)
restconf_get:
path: /data/Cisco-IOS-XE-native:native/ip/access-list/extended
register: result
- name: display output data # (4)
debug:
msg: "{{ result.response }}"
- name: diff intended and outputted acl # (5)
debug:
msg: "{{ content_data['Cisco-IOS-XE-native:ip']['access-list'] | deepdiff(result.response) }}"
4-2. 実行結果(1回目)
- タスク1
TEST2
の順番は、標準出力でYAMLからJSON形式に変換される段階で、TEST
と同じ順番に入れ替わっています。そのため、YAMLで事前定義するデータの順番は意識しなくて良さそうです。 - タスク4
パッと見た感じ、タスク1と同様の結果が出力されています。 - タスク5
いろいろと差分が出てしまいましたorz。 大きくは以下2つです。-
"old_type": "AnsibleUnicode"
と"new_type": "AnsibleUnsafeText"
DeepDiffは、デフォルトでデータのタイプ(型)も見るようです。出力で使うオブジェクトが事前事後で異なるのが原因のようです。 -
"old_type": "int"
と"new_type": "AnsibleUnsafeText"
シーケンス番号も、タイプの違いで差分が出ています。
-
$ ansible-playbook -i inventory_restconf1.ini playbook_restconf_create_acl_diff4.yml
PLAY [cisco] ***********************************************************************************************************************************
TASK [display intended data] *******************************************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"Cisco-IOS-XE-acl:extended": [
{
"access-list-seq-rule": [
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "0.0.0.255",
"ipv4-address": "192.168.4.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": 30
},
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "0.0.0.255",
"ipv4-address": "192.168.5.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": 50
}
],
"name": "TEST"
},
{
"access-list-seq-rule": [
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "255.255.255.0",
"ipv4-address": "192.168.7.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": 70
}
],
"name": "TEST2"
}
]
}
}
TASK [create new acl] **************************************************************************************************************************
changed: [csr1000v-1]
TASK [get acl info] ****************************************************************************************************************************
ok: [csr1000v-1]
TASK [display output data] *********************************************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"Cisco-IOS-XE-acl:extended": [
{
"access-list-seq-rule": [
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "0.0.0.255",
"ipv4-address": "192.168.4.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": "30"
},
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "0.0.0.255",
"ipv4-address": "192.168.5.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": "50"
}
],
"name": "TEST"
},
{
"access-list-seq-rule": [
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0",
"dest-mask": "255.255.255.0",
"ipv4-address": "192.168.7.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": "70"
}
],
"name": "TEST2"
}
]
}
}
TASK [diff intended and outputted acl] *********************************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"type_changes": {
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['action']": {
"new_type": "AnsibleUnsafeText",
"new_value": "permit",
"old_type": "AnsibleUnicode",
"old_value": "permit"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['dest-ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.100.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.100.0"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['dest-mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "0.0.0.255",
"old_type": "AnsibleUnicode",
"old_value": "0.0.0.255"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.4.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.4.0"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "0.0.0.255",
"old_type": "AnsibleUnicode",
"old_value": "0.0.0.255"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['ace-rule']['protocol']": {
"new_type": "AnsibleUnsafeText",
"new_value": "ip",
"old_type": "AnsibleUnicode",
"old_value": "ip"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['sequence']": {
"new_type": "AnsibleUnsafeText",
"new_value": "30",
"old_type": "int",
"old_value": 30
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['action']": {
"new_type": "AnsibleUnsafeText",
"new_value": "permit",
"old_type": "AnsibleUnicode",
"old_value": "permit"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['dest-ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.100.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.100.0"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['dest-mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "0.0.0.255",
"old_type": "AnsibleUnicode",
"old_value": "0.0.0.255"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.5.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.5.0"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "0.0.0.255",
"old_type": "AnsibleUnicode",
"old_value": "0.0.0.255"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['ace-rule']['protocol']": {
"new_type": "AnsibleUnsafeText",
"new_value": "ip",
"old_type": "AnsibleUnicode",
"old_value": "ip"
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['sequence']": {
"new_type": "AnsibleUnsafeText",
"new_value": "50",
"old_type": "int",
"old_value": 50
},
"root['Cisco-IOS-XE-acl:extended'][0]['name']": {
"new_type": "AnsibleUnsafeText",
"new_value": "TEST",
"old_type": "AnsibleUnicode",
"old_value": "TEST"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['action']": {
"new_type": "AnsibleUnsafeText",
"new_value": "permit",
"old_type": "AnsibleUnicode",
"old_value": "permit"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['dest-ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.100.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.100.0"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['dest-mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "255.255.255.0",
"old_type": "AnsibleUnicode",
"old_value": "255.255.255.0"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['ipv4-address']": {
"new_type": "AnsibleUnsafeText",
"new_value": "192.168.7.0",
"old_type": "AnsibleUnicode",
"old_value": "192.168.7.0"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['mask']": {
"new_type": "AnsibleUnsafeText",
"new_value": "0.0.0.255",
"old_type": "AnsibleUnicode",
"old_value": "0.0.0.255"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['ace-rule']['protocol']": {
"new_type": "AnsibleUnsafeText",
"new_value": "ip",
"old_type": "AnsibleUnicode",
"old_value": "ip"
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['sequence']": {
"new_type": "AnsibleUnsafeText",
"new_value": "70",
"old_type": "int",
"old_value": 70
},
"root['Cisco-IOS-XE-acl:extended'][1]['name']": {
"new_type": "AnsibleUnsafeText",
"new_value": "TEST2",
"old_type": "AnsibleUnicode",
"old_value": "TEST2"
}
}
}
}
PLAY RECAP *************************************************************************************************************************************
csr1000v-1 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4-3. 実行(2回目)
DeepDiffには、比較時のオプションとしてignore_type_in_groups
があります。これは、事前事後のタイプが特定の組み合わせの場合、「タイプ」の差分比較を無視するものです(データそのものは差分比較する。)
今回は、フィルタープラグインに直接オプションを追加しました。具体的には以下の通り、差分が出てしまった2つのタイプの組み合わせを指定しました。加えて、冒頭で関連するオブジェクトもインポートしました。(うーん、なんか微妙な気が。。)
# ~省略~
try:
from deepdiff import DeepDiff # For Deep Difference of 2 objects
from ansible.parsing.yaml.objects import AnsibleUnicode # 追加
from ansible.utils.unsafe_proxy import AnsibleUnsafeText # 追加
HAS_DEEPDIFF = True
except ImportError:
HAS_DEEPDIFF = False
# ~省略~
def deepdiff(self, output1, output2):
# ~省略~
ddiff = DeepDiff(output1, output2, ignore_type_in_groups=[(AnsibleUnicode, AnsibleUnsafeText),(AnsibleUnsafeText, int)]) # 修正
# ~省略~
4-4. 実行結果(2回目)
1回目で設定したACLを一旦削除し、同じPlaybookを実行した結果です。
タスク1~4は同じ結果なので割愛します。タスク5を見ると、まだ差分があります!
タイプの違いは見なくなりましたが、シーケンス番号の値そのものが違うと怒られましたorz
$ ansible-playbook -i inventory_restconf1.ini playbook_restconf_create_acl_diff4.yml
PLAY [cisco] ***********************************************************************************************************************************
# ~省略~
TASK [diff intended and outputted acl] *********************************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"values_changed": {
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][0]['sequence']": {
"new_value": "30",
"old_value": 30
},
"root['Cisco-IOS-XE-acl:extended'][0]['access-list-seq-rule'][1]['sequence']": {
"new_value": "50",
"old_value": 50
},
"root['Cisco-IOS-XE-acl:extended'][1]['access-list-seq-rule'][0]['sequence']": {
"new_value": "70",
"old_value": 70
}
}
}
}
PLAY RECAP *************************************************************************************************************************************
csr1000v-1 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4-5. 実行(3回目)
プレイ変数内のsequence
がint型にならないよう、sequence: xx
からsequence: "xx"
に修正しました。
vars:
content_data:
Cisco-IOS-XE-native:ip:
access-list:
Cisco-IOS-XE-acl:extended:
- access-list-seq-rule:
- ace-rule:
action: permit
dest-ipv4-address: 192.168.100.0
dest-mask: 0.0.0.255
ipv4-address: 192.168.4.0
mask: 0.0.0.255
protocol: ip
sequence: "30"
- ace-rule:
action: permit
dest-ipv4-address: 192.168.100.0
dest-mask: 0.0.0.255
ipv4-address: 192.168.5.0
mask: 0.0.0.255
protocol: ip
sequence: "50"
name: TEST
- name: TEST2
access-list-seq-rule:
- sequence: "70"
ace-rule:
action: permit
mask: 0.0.0.255
dest-ipv4-address: 192.168.100.0
dest-mask: 255.255.255.0
ipv4-address: 192.168.7.0
protocol: ip
4-6. 実行結果(3回目)
タスク5の差分がなくなりました!!
$ ansible-playbook -i inventory_restconf1.ini playbook_restconf_create_acl_diff4.yml
PLAY [cisco] ***********************************************************************************************************************************
# ~省略~
TASK [diff intended and outputted acl] *********************************************************************************************************
ok: [csr1000v-1] => {
"msg": {}
}
PLAY RECAP *************************************************************************************************************************************
csr1000v-1 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4-7. 残りの差分について
そういえば、ワイルドカードマスクの誤りで差分が出るのを期待していましたが、出ませんでしたね。
設定するACLをプレイ変数
content_data
で定義。name: TEST2
以降は、各要素の順番をバラバラに入れ替えています。また、宛先のワイルドカードマスクをあえて誤った設定dest-mask: 255.255.255.0
にしています(本来はdest-mask: 0.0.0.255
)。
このパターンは、本来はアドレスが"0.0.0.0"に書き換わってしまいますが、JSONデータを見ると、"192.168.100.0"のままです。
{
"access-list-seq-rule": [
{
"ace-rule": {
"action": "permit",
"dest-ipv4-address": "192.168.100.0", # ココ
"dest-mask": "255.255.255.0",
"ipv4-address": "192.168.7.0",
"mask": "0.0.0.255",
"protocol": "ip"
},
"sequence": "70"
}
],
"name": "TEST2"
}
ここで、SSHログインしてコンフィグを見てみます。
csr1000v-1#sh run | begin TEST
ip access-list extended TEST
permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
permit ip 192.168.5.0 0.0.0.255 192.168.100.0 0.0.0.255
ip access-list extended TEST2
permit ip 192.168.7.0 0.0.0.255 0.0.0.0 255.255.255.0 # ココ
想定通り"0.0.0.0"になっています。
最後に
今回は、日々の作業でよく行う差分比較をJSONに対し行ってみました。専用のAnsibleフィルタープラグインを用意し、データの型に注意することで比較まで出来ました。
ただし、RESTCONFで確認できる設定情報と、実際に動いているコンフィグ情報に差分が生じることが分かったため、差分確認はコンフィグに対して行うべきかと思います。
今後改修されるかもしれませんが、今のところ 信用できるのはコンフィグだけ!!!