3 from ansible.module_utils.basic import AnsibleModule
17 short_description: Modifies json data inside a file
19 - This module modifies a file containing a json.
20 - It is leveraging jsonpointer module implementing RFC6901:
21 https://pypi.org/project/jsonpointer/
22 https://tools.ietf.org/html/rfc6901
23 - If the file does not exist the module will create it automatically.
28 - The json file to modify.
36 - Pointer to the key inside the json object.
37 - You can leave out the leading slash '/'. It will be prefixed by the
38 module for convenience ('key' equals '/key').
39 - Empty key '' designates the whole JSON document (RFC6901)
40 - Key '/' is valid too and it translates to '' ("": "some value").
41 - The last object in the pointer can be missing but the intermediary
46 - Value to be added/changed for the key specified by pointer.
47 - In the case of 'state = absent' the module will delete those elements
48 described in the value. If the whole key/value should be deleted then
49 value must be set to the empty string '' !
53 - It states either that the combination of key and value should be
55 - If 'present' then the exact results depends on 'action' argument.
56 - If 'absent' and key does not exists - no change, if does exist but
57 'value' is unapplicable (old value is dict, but new is not), then the
58 module will raise error. Special 'value' for state 'absent' is an empty
59 string '' (read above). If 'value' is applicable (both key and value is
60 dict or list) then it will remove only those explicitly named elements.
61 Please beware that if you want to remove key/value pairs from dict then
62 you must provide as 'value' a valid dict - that means key/value pair(s)
63 in curls {}. Here you can use just some dummy value like "". The values
64 can differ, the key/value pair will be deleted if key matches.
65 For example to delete key "xyz" from json object, you must provide
66 'value' similar to this: { "key": ""}
74 - It modifies a presence of the key/value pair when state is 'present'
76 - 'add' is default and means that combination of key/value will be added
77 if not already there. If there is already an old value then it is
78 expected that the old value and the new value are of the same type.
79 Otherwise the module will fail. By the same type we mean that both of
80 them are either scalars (strings, numbers), lists or dicts.
81 - In the case of scalar values everything is simple - if there is already
82 a value, nothing happens.
83 - In the case of lists the module ensures that all components of the new
84 value list are present in the result - it will extend an old value list
85 with the elements of the new value list.
86 - In the case of dicts the missing key/value pairs are added but those
87 already present are preserved - it will NOT overwrite old values.
88 - 'Update' is identical to 'add', but it WILL overwrite old values. For
89 list values this has no meaning, so it behaves like add - it simply
90 merges two lists (extends the old with new).
91 - 'replace' will (re)create key/value combination from scratch - it means
92 that the old value is completely discarded if there is any.
103 if os.path.exists(path):
104 with open(path, 'r') as f:
110 def store_json(path, json_data):
111 with open(path, 'w') as f:
112 json.dump(json_data, f, indent=4)
116 def modify_json(json_data, pointer, json_value, state='present', action='add'):
117 is_root = False # special treatment - we cannot modify reference in place
121 value = json.loads(json_value)
125 if state == 'present':
126 if action not in ['add', 'update', 'replace']:
128 elif state == 'absent':
133 # we store the original json document to compare it later
134 original_json_data = copy.deepcopy(json_data)
137 target = jsonpointer.resolve_pointer(json_data, pointer)
141 except jsonpointer.JsonPointerException:
145 if state == "present":
147 if isinstance(target, dict) and isinstance(value, dict):
148 # we keep old values and only append new ones
150 result = jsonpointer.set_pointer(json_data,
153 inplace=(not is_root))
156 elif isinstance(target, list) and isinstance(value, list):
157 # we just append new items to the list
159 if item not in target:
161 elif ((not isinstance(target, dict)) and
162 (not isinstance(target, list))):
163 # 'add' does not overwrite
167 elif action == "update":
168 if isinstance(target, dict) and isinstance(value, dict):
169 # we append new values and overwrite the old ones
171 elif isinstance(target, list) and isinstance(value, list):
172 # we just append new items to the list - same as with 'add'
174 if item not in target:
176 elif ((not isinstance(target, dict)) and
177 (not isinstance(target, list))):
178 # 'update' DOES overwrite
179 if value is not None:
180 result = jsonpointer.set_pointer(json_data,
183 elif target != json_value:
184 result = jsonpointer.set_pointer(json_data,
191 elif action == "replace":
192 # simple case when we don't care what was there before (almost)
193 if value is not None:
194 result = jsonpointer.set_pointer(json_data,
197 inplace=(not is_root))
199 result = jsonpointer.set_pointer(json_data,
202 inplace=(not is_root))
207 elif state == "absent":
208 # we will delete the elements in the object or object itself
211 # we just return empty json
213 elif isinstance(target, dict) and isinstance(value, dict):
215 target.pop(key, None)
219 # we must take a step back in the pointer, so we can edit it
220 ppointer = pointer.split('/')
221 to_delete = ppointer.pop()
222 ppointer = '/'.join(ppointer)
223 ptarget = jsonpointer.resolve_pointer(json_data, ppointer)
224 if (((not isinstance(target, dict)) and
225 (not isinstance(target, list)) and
227 (isinstance(target, dict) or
228 isinstance(target, list)) and
230 # we simply delete the key with it's value (whatever it is)
231 ptarget.pop(to_delete, None)
232 target = ptarget # piece of self-defense
233 elif isinstance(target, dict) and isinstance(value, dict):
235 target.pop(key, None)
236 elif isinstance(target, list) and isinstance(value, list):
247 # the simplest case - nothing was there before and pointer is not root
248 # because in that case we would have key_exists = true
249 if state == 'present':
250 if value is not None:
251 result = jsonpointer.set_pointer(json_data,
255 result = jsonpointer.set_pointer(json_data,
259 if json_data != original_json_data:
265 msg = "JSON object '%s' was updated" % pointer
267 msg = "No change to JSON object '%s'" % pointer
269 return json_data, changed, msg
273 module = AnsibleModule(
275 path=dict(type='path', required=True,
276 aliases=['name', 'destfile', 'dest']),
277 key=dict(type='str', required=True),
278 value=dict(type='str', required=True),
279 state=dict(default='present', choices=['present', 'absent']),
280 action=dict(required=False, default='add',
285 supports_check_mode=True
288 if jsonpointer is None:
289 module.fail_json(msg='jsonpointer module is not available')
291 path = module.params['path']
292 pointer = module.params['key']
293 value = module.params['value']
294 state = module.params['state']
295 action = module.params['action']
297 if pointer == '' or pointer == '/':
299 elif not pointer.startswith("/"):
300 pointer = "/" + pointer
303 json_data = load_json(path)
304 except Exception as err:
305 module.fail_json(msg=str(err))
308 json_data, changed, msg = modify_json(json_data,
313 except jsonpointer.JsonPointerException as err:
314 module.fail_json(msg=str(err))
315 except ValueError as err:
316 module.fail_json(msg="Wrong usage of state, action and/or key/value")
319 if not module.check_mode and changed:
320 store_json(path, json_data)
321 except IOError as err:
322 module.fail_json(msg=str(err))
324 module.exit_json(changed=changed, msg=msg)
327 if __name__ == '__main__':