You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

360 lines
10 KiB

  1. #!/usr/bin/python
  2. ANSIBLE_METADATA = {
  3. 'metadata_version': '1.1',
  4. 'status': ['preview'],
  5. 'supported_by': 'community'
  6. }
  7. DOCUMENTATION = '''
  8. ---
  9. module: jsonconfig
  10. short_description: Ensure a particular configuration is added to a json-formatted configuration file
  11. version_added: "2.4"
  12. description:
  13. - This module will add configuration to a json-formatted configuration file.
  14. options:
  15. dest:
  16. description:
  17. - The file to modify.
  18. required: true
  19. aliases: [ name, destfile ]
  20. json:
  21. description:
  22. - The configuration in json format to apply.
  23. required: false
  24. default: '{}'
  25. merge:
  26. description:
  27. - Used with C(state=present). If specified, it will merge the configuration. Othwerwise
  28. the configuration will be overwritten.
  29. required: false
  30. choices: [ "yes", "no" ]
  31. default: "yes"
  32. state:
  33. description:
  34. - Whether the configuration should be there or not.
  35. required: false
  36. choices: [ present, absent ]
  37. default: "present"
  38. create:
  39. description:
  40. - Used with C(state=present). If specified, the file will be created
  41. if it does not already exist. By default it will fail if the file
  42. is missing.
  43. required: false
  44. choices: [ "yes", "no" ]
  45. default: "no"
  46. backup:
  47. description:
  48. - Create a backup file including the timestamp information so you can
  49. get the original file back if you somehow clobbered it incorrectly.
  50. required: false
  51. choices: [ "yes", "no" ]
  52. default: "no"
  53. others:
  54. description:
  55. - All arguments accepted by the M(file) module also work here.
  56. required: false
  57. extends_documentation_fragment:
  58. - files
  59. - validate
  60. author:
  61. - "Greg Szabo (@greg-szabo)"
  62. '''
  63. EXAMPLES = '''
  64. # Add a new section to a json file
  65. - name: Add comment section
  66. jsonconfig:
  67. dest: /etc/something.json
  68. json: '{ "comment": { "comment1": "mycomment" } }'
  69. # Rewrite a json file with the configuration
  70. - name: Create or overwrite config.json
  71. jsonconfig:
  72. dest: /etc/config.json
  73. json: '{ "regedit": { "freshfile": true } }'
  74. merge: no
  75. create: yes
  76. '''
  77. RETURN = '''
  78. changed:
  79. description: True if the configuration changed.
  80. type: bool
  81. msg:
  82. description: Description of the change
  83. type: str
  84. '''
  85. from ansible.module_utils.basic import AnsibleModule
  86. from ansible.module_utils.six import b
  87. from ansible.module_utils._text import to_bytes, to_native
  88. import tempfile
  89. import json
  90. import copy
  91. import os
  92. def write_changes(module, b_lines, dest):
  93. tmpfd, tmpfile = tempfile.mkstemp()
  94. f = os.fdopen(tmpfd, 'wb')
  95. f.writelines(b_lines)
  96. f.close()
  97. validate = module.params.get('validate', None)
  98. valid = not validate
  99. if validate:
  100. if "%s" not in validate:
  101. module.fail_json(msg="validate must contain %%s: %s" % (validate))
  102. (rc, out, err) = module.run_command(to_bytes(validate % tmpfile, errors='surrogate_or_strict'))
  103. valid = rc == 0
  104. if rc != 0:
  105. module.fail_json(msg='failed to validate: '
  106. 'rc:%s error:%s' % (rc, err))
  107. if valid:
  108. module.atomic_move(tmpfile,
  109. to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'),
  110. unsafe_writes=module.params['unsafe_writes'])
  111. def check_file_attrs(module, changed, message, diff):
  112. file_args = module.load_file_common_arguments(module.params)
  113. if module.set_fs_attributes_if_different(file_args, False, diff=diff):
  114. if changed:
  115. message += " and "
  116. changed = True
  117. message += "ownership, perms or SE linux context changed"
  118. return message, changed
  119. #Merge dict d2 into dict d1 and return a new object
  120. def deepmerge(d1, d2):
  121. if d1 is None:
  122. return copy.deepcopy(d2)
  123. if d2 is None:
  124. return copy.deepcopy(d1)
  125. if d1 == d2:
  126. return copy.deepcopy(d1)
  127. if isinstance(d1, dict) and isinstance(d2, dict):
  128. result={}
  129. for key in set(d1.keys()+d2.keys()):
  130. da = db = None
  131. if key in d1:
  132. da = d1[key]
  133. if key in d2:
  134. db = d2[key]
  135. result[key] = deepmerge(da, db)
  136. return result
  137. else:
  138. return copy.deepcopy(d2)
  139. #Remove dict d2 from dict d1 and return a new object
  140. def deepdiff(d1, d2):
  141. if d1 is None or d2 is None:
  142. return None
  143. if d1 == d2:
  144. return None
  145. if isinstance(d1, dict) and isinstance(d2, dict):
  146. result = {}
  147. for key in d1.keys():
  148. if key in d2:
  149. dd = deepdiff(d1[key],d2[key])
  150. if dd is not None:
  151. result[key] = dd
  152. else:
  153. result[key] = d1[key]
  154. return result
  155. else:
  156. return None
  157. def present(module, dest, conf, merge, create, backup):
  158. diff = {'before': '',
  159. 'after': '',
  160. 'before_header': '%s (content)' % dest,
  161. 'after_header': '%s (content)' % dest}
  162. b_dest = to_bytes(dest, errors='surrogate_or_strict')
  163. if not os.path.exists(b_dest):
  164. if not create:
  165. module.fail_json(rc=257, msg='Destination %s does not exist !' % dest)
  166. b_destpath = os.path.dirname(b_dest)
  167. if not os.path.exists(b_destpath) and not module.check_mode:
  168. os.makedirs(b_destpath)
  169. b_lines = []
  170. else:
  171. f = open(b_dest, 'rb')
  172. b_lines = f.readlines()
  173. f.close()
  174. lines = to_native(b('').join(b_lines))
  175. if module._diff:
  176. diff['before'] = lines
  177. b_conf = to_bytes(conf, errors='surrogate_or_strict')
  178. jsonconfig = json.loads(lines)
  179. config = eval(b_conf)
  180. if not isinstance(config, dict):
  181. module.fail_json(msg="Invalid value in json parameter: {0}".format(config))
  182. b_lines_new = b_lines
  183. msg = ''
  184. changed = False
  185. if not merge:
  186. if jsonconfig != config:
  187. b_lines_new = to_bytes(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')))
  188. msg = 'config overwritten'
  189. changed = True
  190. else:
  191. mergedconfig = deepmerge(jsonconfig,config)
  192. if jsonconfig != mergedconfig:
  193. b_lines_new = to_bytes(json.dumps(mergedconfig, sort_keys=True, indent=4, separators=(',', ': ')))
  194. msg = 'config merged'
  195. changed = True
  196. if module._diff:
  197. diff['after'] = to_native(b('').join(b_lines_new))
  198. backupdest = ""
  199. if changed and not module.check_mode:
  200. if backup and os.path.exists(b_dest):
  201. backupdest = module.backup_local(dest)
  202. write_changes(module, b_lines_new, dest)
  203. if module.check_mode and not os.path.exists(b_dest):
  204. module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=diff)
  205. attr_diff = {}
  206. msg, changed = check_file_attrs(module, changed, msg, attr_diff)
  207. attr_diff['before_header'] = '%s (file attributes)' % dest
  208. attr_diff['after_header'] = '%s (file attributes)' % dest
  209. difflist = [diff, attr_diff]
  210. module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist)
  211. def absent(module, dest, conf, backup):
  212. b_dest = to_bytes(dest, errors='surrogate_or_strict')
  213. if not os.path.exists(b_dest):
  214. module.exit_json(changed=False, msg="file not present")
  215. msg = ''
  216. diff = {'before': '',
  217. 'after': '',
  218. 'before_header': '%s (content)' % dest,
  219. 'after_header': '%s (content)' % dest}
  220. f = open(b_dest, 'rb')
  221. b_lines = f.readlines()
  222. f.close()
  223. lines = to_native(b('').join(b_lines))
  224. b_conf = to_bytes(conf, errors='surrogate_or_strict')
  225. lines = to_native(b('').join(b_lines))
  226. jsonconfig = json.loads(lines)
  227. config = eval(b_conf)
  228. if not isinstance(config, dict):
  229. module.fail_json(msg="Invalid value in json parameter: {0}".format(config))
  230. if module._diff:
  231. diff['before'] = to_native(b('').join(b_lines))
  232. b_lines_new = b_lines
  233. msg = ''
  234. changed = False
  235. diffconfig = deepdiff(jsonconfig,config)
  236. if diffconfig is None:
  237. diffconfig = {}
  238. if jsonconfig != diffconfig:
  239. b_lines_new = to_bytes(json.dumps(diffconfig, sort_keys=True, indent=4, separators=(',', ': ')))
  240. msg = 'config removed'
  241. changed = True
  242. if module._diff:
  243. diff['after'] = to_native(b('').join(b_lines_new))
  244. backupdest = ""
  245. if changed and not module.check_mode:
  246. if backup:
  247. backupdest = module.backup_local(dest)
  248. write_changes(module, b_lines_new, dest)
  249. attr_diff = {}
  250. msg, changed = check_file_attrs(module, changed, msg, attr_diff)
  251. attr_diff['before_header'] = '%s (file attributes)' % dest
  252. attr_diff['after_header'] = '%s (file attributes)' % dest
  253. difflist = [diff, attr_diff]
  254. module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist)
  255. def main():
  256. # define the available arguments/parameters that a user can pass to
  257. # the module
  258. module_args = dict(
  259. dest=dict(type='str', required=True),
  260. json=dict(default=None, required=True),
  261. merge=dict(type='bool', default=True),
  262. state=dict(default='present', choices=['absent', 'present']),
  263. create=dict(type='bool', default=False),
  264. backup=dict(type='bool', default=False),
  265. validate=dict(default=None, type='str')
  266. )
  267. # the AnsibleModule object will be our abstraction working with Ansible
  268. # this includes instantiation, a couple of common attr would be the
  269. # args/params passed to the execution, as well as if the module
  270. # supports check mode
  271. module = AnsibleModule(
  272. argument_spec=module_args,
  273. add_file_common_args=True,
  274. supports_check_mode=True
  275. )
  276. params = module.params
  277. create = params['create']
  278. merge = params['merge']
  279. backup = params['backup']
  280. dest = params['dest']
  281. b_dest = to_bytes(dest, errors='surrogate_or_strict')
  282. if os.path.isdir(b_dest):
  283. module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
  284. conf = params['json']
  285. if params['state'] == 'present':
  286. present(module, dest, conf, merge, create, backup)
  287. else:
  288. absent(module, dest, conf, backup)
  289. if __name__ == '__main__':
  290. main()