#!/usr/bin/python from ansible.module_utils.basic import AnsibleModule import os import copy import json try: import jsonpointer except ImportError: jsonpointer = None DOCUMENTATION = """ --- module: json_mod short_description: Modifies json data inside a file description: - This module modifies a file containing a json. - It is leveraging jsonpointer module implementing RFC6901: https://pypi.org/project/jsonpointer/ https://tools.ietf.org/html/rfc6901 - If the file does not exist the module will create it automatically. options: path: description: - The json file to modify. required: true aliases: - name - destfile - dest key: description: - Pointer to the key inside the json object. - You can leave out the leading slash '/'. It will be prefixed by the module for convenience ('key' equals '/key'). - Empty key '' designates the whole JSON document (RFC6901) - Key '/' is valid too and it translates to '' ("": "some value"). - The last object in the pointer can be missing but the intermediary objects must exist. required: true value: description: - Value to be added/changed for the key specified by pointer. - In the case of 'state = absent' the module will delete those elements described in the value. If the whole key/value should be deleted then value must be set to the empty string '' ! required: true state: description: - It states either that the combination of key and value should be present or absent. - If 'present' then the exact results depends on 'action' argument. - If 'absent' and key does not exists - no change, if does exist but 'value' is unapplicable (old value is dict, but new is not), then the module will raise error. Special 'value' for state 'absent' is an empty string '' (read above). If 'value' is applicable (both key and value is dict or list) then it will remove only those explicitly named elements. Please beware that if you want to remove key/value pairs from dict then you must provide as 'value' a valid dict - that means key/value pair(s) in curls {}. Here you can use just some dummy value like "". The values can differ, the key/value pair will be deleted if key matches. For example to delete key "xyz" from json object, you must provide 'value' similar to this: { "key": ""} required: false default: present choices: - present - absent action: description: - It modifies a presence of the key/value pair when state is 'present' otherwise is ignored. - 'add' is default and means that combination of key/value will be added if not already there. If there is already an old value then it is expected that the old value and the new value are of the same type. Otherwise the module will fail. By the same type we mean that both of them are either scalars (strings, numbers), lists or dicts. - In the case of scalar values everything is simple - if there is already a value, nothing happens. - In the case of lists the module ensures that all components of the new value list are present in the result - it will extend an old value list with the elements of the new value list. - In the case of dicts the missing key/value pairs are added but those already present are preserved - it will NOT overwrite old values. - 'Update' is identical to 'add', but it WILL overwrite old values. For list values this has no meaning, so it behaves like add - it simply merges two lists (extends the old with new). - 'replace' will (re)create key/value combination from scratch - it means that the old value is completely discarded if there is any. required: false default: add choices: - add - update - replace """ def load_json(path): if os.path.exists(path): with open(path, 'r') as f: return json.load(f) else: return {} def store_json(path, json_data): with open(path, 'w') as f: json.dump(json_data, f, indent=4) f.write("\n") def modify_json(json_data, pointer, json_value, state='present', action='add'): is_root = False # special treatment - we cannot modify reference in place key_exists = False try: value = json.loads(json_value) except Exception: value = None if state == 'present': if action not in ['add', 'update', 'replace']: raise ValueError elif state == 'absent': pass else: raise ValueError # we store the original json document to compare it later original_json_data = copy.deepcopy(json_data) try: target = jsonpointer.resolve_pointer(json_data, pointer) if pointer == '': is_root = True key_exists = True except jsonpointer.JsonPointerException: key_exists = False if key_exists: if state == "present": if action == "add": if isinstance(target, dict) and isinstance(value, dict): # we keep old values and only append new ones value.update(target) result = jsonpointer.set_pointer(json_data, pointer, value, inplace=(not is_root)) if is_root: json_data = result elif isinstance(target, list) and isinstance(value, list): # we just append new items to the list for item in value: if item not in target: target.append(item) elif ((not isinstance(target, dict)) and (not isinstance(target, list))): # 'add' does not overwrite pass else: raise ValueError elif action == "update": if isinstance(target, dict) and isinstance(value, dict): # we append new values and overwrite the old ones target.update(value) elif isinstance(target, list) and isinstance(value, list): # we just append new items to the list - same as with 'add' for item in value: if item not in target: target.append(item) elif ((not isinstance(target, dict)) and (not isinstance(target, list))): # 'update' DOES overwrite if value is not None: result = jsonpointer.set_pointer(json_data, pointer, value) elif target != json_value: result = jsonpointer.set_pointer(json_data, pointer, json_value) else: raise ValueError else: raise ValueError elif action == "replace": # simple case when we don't care what was there before (almost) if value is not None: result = jsonpointer.set_pointer(json_data, pointer, value, inplace=(not is_root)) else: result = jsonpointer.set_pointer(json_data, pointer, json_value, inplace=(not is_root)) if is_root: json_data = result else: raise ValueError elif state == "absent": # we will delete the elements in the object or object itself if is_root: if json_value == '': # we just return empty json json_data = {} elif isinstance(target, dict) and isinstance(value, dict): for key in value: target.pop(key, None) else: raise ValueError else: # we must take a step back in the pointer, so we can edit it ppointer = pointer.split('/') to_delete = ppointer.pop() ppointer = '/'.join(ppointer) ptarget = jsonpointer.resolve_pointer(json_data, ppointer) if (((not isinstance(target, dict)) and (not isinstance(target, list)) and json_value == '') or (isinstance(target, dict) or isinstance(target, list)) and json_value == ''): # we simply delete the key with it's value (whatever it is) ptarget.pop(to_delete, None) target = ptarget # piece of self-defense elif isinstance(target, dict) and isinstance(value, dict): for key in value: target.pop(key, None) elif isinstance(target, list) and isinstance(value, list): for item in value: try: target.remove(item) except ValueError: pass else: raise ValueError else: raise ValueError else: # the simplest case - nothing was there before and pointer is not root # because in that case we would have key_exists = true if state == 'present': if value is not None: result = jsonpointer.set_pointer(json_data, pointer, value) else: result = jsonpointer.set_pointer(json_data, pointer, json_value) if json_data != original_json_data: changed = True else: changed = False if changed: msg = "JSON object '%s' was updated" % pointer else: msg = "No change to JSON object '%s'" % pointer return json_data, changed, msg def main(): module = AnsibleModule( argument_spec=dict( path=dict(type='path', required=True, aliases=['name', 'destfile', 'dest']), key=dict(type='str', required=True), value=dict(type='str', required=True), state=dict(default='present', choices=['present', 'absent']), action=dict(required=False, default='add', choices=['add', 'update', 'replace']), ), supports_check_mode=True ) if jsonpointer is None: module.fail_json(msg='jsonpointer module is not available') path = module.params['path'] pointer = module.params['key'] value = module.params['value'] state = module.params['state'] action = module.params['action'] if pointer == '' or pointer == '/': pass elif not pointer.startswith("/"): pointer = "/" + pointer try: json_data = load_json(path) except Exception as err: module.fail_json(msg=str(err)) try: json_data, changed, msg = modify_json(json_data, pointer, value, state, action) except jsonpointer.JsonPointerException as err: module.fail_json(msg=str(err)) except ValueError as err: module.fail_json(msg="Wrong usage of state, action and/or key/value") try: if not module.check_mode and changed: store_json(path, json_data) except IOError as err: module.fail_json(msg=str(err)) module.exit_json(changed=changed, msg=msg) if __name__ == '__main__': main()