はじめに
以前の記事で、事前に定義したJSONデータと、設定後のJSONデータを差分比較し、想定通り設定が反映されているか確認しました。
今回は2つのJSONデータを、Ansibleを使ってマージしたいと思います。具体的には、Cisco IOS-XEのインターフェース設定をRESTCONFで行う前に、既存設定と追加設定をマージしてみます。
1. 用意した環境
Cisco DevNet SandboxのIOS XE on CSR Recommended CodeのCSR1000v 16.9.3を利用させて頂きました。
Python3.6.8のvenv仮想環境上で、Ansible2.9.2をインストールして使いました。
2. フィルタープラグイン
JSONMergeをAnsibleフィルタープラグインとして取り込みました。こちらはPythonベースのモジュールで、base(元々のJSONデータ)に対し、head(追加のJSONデータ)をマージして結果を出力してくれます。
Merge Strategiesでマージ方法を指定できます。特に指定しない場合はoverwrite
となり、古い値が上書されてしまいます。実際には、配列データ[ 〇, △ ]
は要素を追加したいケースが多いと思います。その場合は、append
(単純に追加)やarrayMergeById
(要素内の特定のkeyの値が同じものはマージ、新しいものは追加)等を選択可能です。
実例は後ほどご紹介しますが、どのデータをどう処理するかは、JSONスキーマと同じ要領で指定できます。
2-1. 事前準備
JSONMergeのインストールを行います。
$ pip install jsonmerge
2-2. Pythonコード(抜粋)
こちらの記事と同様、filter_plugins
ディレクトリ内に以下のファイルを格納しました。(実際には、他のカスタムフィルターも同ファイル内に書いています。)
やっている事は以下の2点です。
-
HAS_JSONMERGE
で、JSONMergeがインストールされているか判定 - JSONスキーマに従い、2つのJSONデータをマージしたものを返す
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 jsonmerge import Merger
HAS_JSONMERGE = True
except ImportError:
HAS_JSONMERGE = False
class FilterModule(object):
def jsonmerge(self, output1, output2, schema):
if not HAS_JSONMERGE:
raise AnsibleFilterError("JSONMerge not found. Run 'pip install jsonmerge'")
merger = Merger(schema)
merge_result = merger.merge(output1, output2)
return merge_result
def filters(self):
return {
'jsonmerge': self.jsonmerge
}
3. Inventoryファイル
以前の記事と同じものを使いました。
4. Playbook
各タスクの内容は以下の通りです。
-
restconf_get
モジュールで、既存のインターフェース設定を取得(GigabieEthernet1~3
の内、1は設定済み) - タスク1の結果を出力
- Playbook変数
content_data
内に、GigabitEthernet2
のDescription、閉塞解除(no shutdown)、IPアドレス設定を定義。
タスク1の結果と、content_data
をフィルタープラグインjsonmerge
でマージ。
この時、タスク変数schema
で、インターフェース設定(interface:
)のMerge StrategyをarrayMergeById
に指定。インターフェース名をIDとして(idRef: name
)、同じIDは設定をマージするよう指定。
---
- hosts: cisco
gather_facts: no
vars:
content_data:
ietf-interfaces:interfaces:
interface:
- description: "For TEST"
enabled: true
ietf-ip:ipv4:
address:
- ip: 192.168.1.1
netmask: 255.255.255.0
name: GigabitEthernet2
tasks:
- name: get interface info # (1)
restconf_get:
path: /data/ietf-interfaces:interfaces
register: result_before
- name: display output data # (2)
debug:
msg: "{{ result_before.response }}"
- name: merge existing and setup interface settings # (3)
debug:
msg: "{{ result_before.response | jsonmerge(content_data, schema) }}"
vars:
schema:
properties:
ietf-interfaces:interfaces:
properties:
interface:
mergeStrategy: arrayMergeById
mergeOptions:
idRef: name
5. 出力結果
タスク3の結果を見ると、問題なくマージされている事が分かります。
$ ansible-playbook -i inventory_restconf1.ini playbook_restconf_int_merge1.yml
PLAY [cisco] **********************************************************************************************************
TASK [get interface info] *********************************************************************************************
ok: [csr1000v-1]
TASK [display output data] ********************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"ietf-interfaces:interfaces": {
"interface": [
{
"description": "MANAGEMENT INTERFACE - DON'T TOUCH ME",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "10.10.20.48",
"netmask": "255.255.255.0"
}
]
},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet1",
"type": "iana-if-type:ethernetCsmacd"
},
{
"enabled": false,
"ietf-ip:ipv4": {},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet2",
"type": "iana-if-type:ethernetCsmacd"
},
{
"description": "Network Interface",
"enabled": false,
"ietf-ip:ipv4": {},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet3",
"type": "iana-if-type:ethernetCsmacd"
}
]
}
}
}
TASK [merge existing and setup interface settings] ********************************************************************
ok: [csr1000v-1] => {
"msg": {
"ietf-interfaces:interfaces": {
"interface": [
{
"description": "MANAGEMENT INTERFACE - DON'T TOUCH ME",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "10.10.20.48",
"netmask": "255.255.255.0"
}
]
},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet1",
"type": "iana-if-type:ethernetCsmacd"
},
{
"description": "For TEST",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "192.168.1.1",
"netmask": "255.255.255.0"
}
]
},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet2",
"type": "iana-if-type:ethernetCsmacd"
},
{
"description": "Network Interface",
"enabled": false,
"ietf-ip:ipv4": {},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet3",
"type": "iana-if-type:ethernetCsmacd"
}
]
}
}
}
PLAY RECAP ************************************************************************************************************
csr1000v-1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
(参考) インターフェース設定変更
ご参考までに、uri
モジュールで実際にインターフェース設定を行った結果を貼り付けておきます。
(参考1) 設定変更のPlaybook
---
- hosts: cisco
gather_facts: no
vars:
content_data:
ietf-interfaces:interfaces:
interface:
- description: "For TEST"
enabled: true
ietf-ip:ipv4:
address:
- ip: 192.168.1.1
netmask: 255.255.255.0
name: GigabitEthernet2
tasks:
- name: merge interface settings
uri:
url: https://{{ansible_host}}:{{ansible_httpapi_port}}/restconf/data/ietf-interfaces:interfaces
method: PATCH
headers:
Content-Type: application/yang-data+json
Accept: application/yang-data+json
body_format: json
body: "{{ content_data | to_json }}"
status_code:
- 200
- 204
url_username: "{{ ansible_user }}"
url_password: "{{ ansible_password }}"
force_basic_auth: yes
validate_certs: no
- name: get interface info
restconf_get:
path: /data/ietf-interfaces:interfaces
register: result
- name: display output data
debug:
msg: "{{ result.response }}"
(参考2) 設定変更の出力結果
$ ansible-playbook -i inventory_restconf1.ini playbook_restconf_int_merge2.yml
PLAY [cisco] **********************************************************************************************************
TASK [merge interface settings] ***************************************************************************************
ok: [csr1000v-1]
TASK [get interface info] *********************************************************************************************
ok: [csr1000v-1]
TASK [display output data] ********************************************************************************************
ok: [csr1000v-1] => {
"msg": {
"ietf-interfaces:interfaces": {
"interface": [
{
"description": "MANAGEMENT INTERFACE - DON'T TOUCH ME",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "10.10.20.48",
"netmask": "255.255.255.0"
}
]
},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet1",
"type": "iana-if-type:ethernetCsmacd"
},
{
"description": "For TEST",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "192.168.1.1",
"netmask": "255.255.255.0"
}
]
},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet2",
"type": "iana-if-type:ethernetCsmacd"
},
{
"description": "Network Interface",
"enabled": false,
"ietf-ip:ipv4": {},
"ietf-ip:ipv6": {},
"name": "GigabitEthernet3",
"type": "iana-if-type:ethernetCsmacd"
}
]
}
}
}
PLAY RECAP ************************************************************************************************************
csr1000v-1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
最後に
今回のケースでは想定通りのマージ結果が得られました。ただし、ネストの関係にある2つ以上の配列データを上書きではなく追加したい場合、JSONスキーマが競合してうまく行きませんでした。何かやり方があるのかもしれませんが、気力の限界に達したので、また今度トライしたいと思います