[ANSIBLE] Add ghcr.io to simulated hosts list
[oom/offline-installer.git] / ansible / library / json_mod.py
1 #!/usr/bin/python
2
3 from ansible.module_utils.basic import AnsibleModule
4
5 import os
6 import copy
7 import json
8
9 try:
10     import jsonpointer
11 except ImportError:
12     jsonpointer = None
13
14 DOCUMENTATION = """
15 ---
16 module: json_mod
17 short_description: Modifies json data inside a file
18 description:
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
25 options:
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
102 def 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
110 def 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
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
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
272 def 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
327 if __name__ == '__main__':
328     main()