blob: 1a95c75ba3e6fb512e4ea221e7492224015dd667 [file] [log] [blame]
Petr Ospalýf604d492019-03-04 06:56:33 +01001#!/usr/bin/python
2
3from ansible.module_utils.basic import AnsibleModule
4
5import os
6import copy
7import json
8
9try:
10 import jsonpointer
11except ImportError:
12 jsonpointer = None
13
14DOCUMENTATION = """
15---
16module: json_mod
17short_description: Modifies json data inside a file
18description:
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.
24
25options:
26 path:
27 description:
28 - The json file to modify.
29 required: true
30 aliases:
31 - name
32 - destfile
33 - dest
34 key:
35 description:
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
42 objects must exist.
43 required: true
44 value:
45 description:
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 '' !
50 required: true
51 state:
52 description:
53 - It states either that the combination of key and value should be
54 present or absent.
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": ""}
67 required: false
68 default: present
69 choices:
70 - present
71 - absent
72 action:
73 description:
74 - It modifies a presence of the key/value pair when state is 'present'
75 otherwise is ignored.
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.
93 required: false
94 default: add
95 choices:
96 - add
97 - update
98 - replace
99"""
100
101
102def load_json(path):
103 if os.path.exists(path):
104 with open(path, 'r') as f:
105 return json.load(f)
106 else:
107 return {}
108
109
110def store_json(path, json_data):
111 with open(path, 'w') as f:
112 json.dump(json_data, f, indent=4)
113 f.write("\n")
114
115
116def modify_json(json_data, pointer, json_value, state='present', action='add'):
117 is_root = False # special treatment - we cannot modify reference in place
118 key_exists = False
119
120 try:
121 value = json.loads(json_value)
122 except Exception:
123 value = None
124
125 if state == 'present':
126 if action not in ['add', 'update', 'replace']:
127 raise ValueError
128 elif state == 'absent':
129 pass
130 else:
131 raise ValueError
132
133 # we store the original json document to compare it later
134 original_json_data = copy.deepcopy(json_data)
135
136 try:
137 target = jsonpointer.resolve_pointer(json_data, pointer)
138 if pointer == '':
139 is_root = True
140 key_exists = True
141 except jsonpointer.JsonPointerException:
142 key_exists = False
143
144 if key_exists:
145 if state == "present":
146 if action == "add":
147 if isinstance(target, dict) and isinstance(value, dict):
148 # we keep old values and only append new ones
149 value.update(target)
150 result = jsonpointer.set_pointer(json_data,
151 pointer,
152 value,
153 inplace=(not is_root))
154 if is_root:
155 json_data = result
156 elif isinstance(target, list) and isinstance(value, list):
157 # we just append new items to the list
158 for item in value:
159 if item not in target:
160 target.append(item)
161 elif ((not isinstance(target, dict)) and
162 (not isinstance(target, list))):
163 # 'add' does not overwrite
164 pass
165 else:
166 raise ValueError
167 elif action == "update":
168 if isinstance(target, dict) and isinstance(value, dict):
169 # we append new values and overwrite the old ones
170 target.update(value)
171 elif isinstance(target, list) and isinstance(value, list):
172 # we just append new items to the list - same as with 'add'
173 for item in value:
174 if item not in target:
175 target.append(item)
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,
181 pointer,
182 value)
183 elif target != json_value:
184 result = jsonpointer.set_pointer(json_data,
185 pointer,
186 json_value)
187 else:
188 raise ValueError
189 else:
190 raise ValueError
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,
195 pointer,
196 value,
197 inplace=(not is_root))
198 else:
199 result = jsonpointer.set_pointer(json_data,
200 pointer,
201 json_value,
202 inplace=(not is_root))
203 if is_root:
204 json_data = result
205 else:
206 raise ValueError
207 elif state == "absent":
208 # we will delete the elements in the object or object itself
209 if is_root:
210 if json_value == '':
211 # we just return empty json
212 json_data = {}
213 elif isinstance(target, dict) and isinstance(value, dict):
214 for key in value:
215 target.pop(key, None)
216 else:
217 raise ValueError
218 else:
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
226 json_value == '') or
227 (isinstance(target, dict) or
228 isinstance(target, list)) and
229 json_value == ''):
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):
234 for key in value:
235 target.pop(key, None)
236 elif isinstance(target, list) and isinstance(value, list):
237 for item in value:
238 try:
239 target.remove(item)
240 except ValueError:
241 pass
242 else:
243 raise ValueError
244 else:
245 raise ValueError
246 else:
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,
252 pointer,
253 value)
254 else:
255 result = jsonpointer.set_pointer(json_data,
256 pointer,
257 json_value)
258
259 if json_data != original_json_data:
260 changed = True
261 else:
262 changed = False
263
264 if changed:
265 msg = "JSON object '%s' was updated" % pointer
266 else:
267 msg = "No change to JSON object '%s'" % pointer
268
269 return json_data, changed, msg
270
271
272def main():
273 module = AnsibleModule(
274 argument_spec=dict(
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',
281 choices=['add',
282 'update',
283 'replace']),
284 ),
285 supports_check_mode=True
286 )
287
288 if jsonpointer is None:
289 module.fail_json(msg='jsonpointer module is not available')
290
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']
296
297 if pointer == '' or pointer == '/':
298 pass
299 elif not pointer.startswith("/"):
300 pointer = "/" + pointer
301
302 try:
303 json_data = load_json(path)
304 except Exception as err:
305 module.fail_json(msg=str(err))
306
307 try:
308 json_data, changed, msg = modify_json(json_data,
309 pointer,
310 value,
311 state,
312 action)
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")
317
318 try:
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))
323
324 module.exit_json(changed=changed, msg=msg)
325
326
327if __name__ == '__main__':
328 main()