1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
|
#!/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()
|