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.

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