AnsibleでJSON形式のデータをマージする

# はじめに

[以前の記事](https://qiita.com/tech_kitara/items/31c9051215725f7135db)で、事前に定義したJSONデータと、設定後のJSONデータを差分比較し、想定通り設定が反映されているか確認しました。
今回は2つのJSONデータを、Ansibleを使ってマージしたいと思います。具体的には、Cisco IOS-XEのインターフェース設定をRESTCONFで行う前に、既存設定と追加設定をマージしてみます。

# 1. 用意した環境
Cisco DevNet Sandboxの[IOS XE on CSR Recommended Code](https://devnetsandbox.cisco.com/RM/Diagram/Index/d6ec9e7c-9cf1-488b-8f1d-876cd9f52cb4)のCSR1000v 16.9.3を利用させて頂きました。
Python3.6.8のvenv仮想環境上で、Ansible2.9.2をインストールして使いました。

# 2. フィルタープラグイン
[JSONMerge](https://github.com/avian2/jsonmerge)をAnsibleフィルタープラグインとして取り込みました。こちらはPythonベースのモジュールで、base（元々のJSONデータ）に対し、head（追加のJSONデータ）をマージして結果を出力してくれます。
Merge Strategiesでマージ方法を指定できます。特に指定しない場合は`overwrite`となり、古い値が上書されてしまいます。実際には、配列データ`[ 〇, △ ]`は要素を追加したいケースが多いと思います。その場合は、`append`（単純に追加）や`arrayMergeById`（要素内の特定のkeyの値が同じものはマージ、新しいものは追加）等を選択可能です。
実例は後ほどご紹介しますが、どのデータをどう処理するかは、[JSONスキーマ](https://json-schema.org/understanding-json-schema/#)と同じ要領で指定できます。

## 2-1. 事前準備
JSONMergeのインストールを行います。

```
$ pip install jsonmerge
```

## 2-2. Pythonコード（抜粋）
[こちらの記事](https://qiita.com/tech_kitara/items/c8879adee80a61379342)と同様、`filter_plugins`ディレクトリ内に以下のファイルを格納しました。（実際には、他のカスタムフィルターも同ファイル内に書いています。）
やっている事は以下の2点です。

- `HAS_JSONMERGE`で、JSONMergeがインストールされているか判定
- JSONスキーマに従い、2つのJSONデータをマージしたものを返す


```python:custom_filters1.py
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ファイル
[以前の記事](https://qiita.com/tech_kitara/items/0e8131afc1fd107ae898#2-inventory%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)と同じものを使いました。


# 4. Playbook
各タスクの内容は以下の通りです。

1. `restconf_get`モジュールで、既存のインターフェース設定を取得（`GigabieEthernet1～3`の内、1は設定済み）
2. タスク1の結果を出力
3. Playbook変数`content_data`内に、`GigabitEthernet2`のDescription、閉塞解除（no shutdown）、IPアドレス設定を定義。  
タスク1の結果と、`content_data`をフィルタープラグイン`jsonmerge`でマージ。  
この時、タスク変数`schema`で、インターフェース設定(`interface:`)のMerge Strategyを`arrayMergeById`に指定。インターフェース名をIDとして（`idRef: name`）、同じIDは設定をマージするよう指定。

```yaml:playbook_restconf_int_merge1.yml
---

- 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
```yaml:playbook_restconf_int_merge2.yml
---

- 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スキーマが競合してうまく行きませんでした。何かやり方があるのかもしれませんが、気力の限界に達したので、また今度トライしたいと思います:sweat: