#!/usr/bin/python ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = ''' --- module: tomlconfig short_description: Ensure a particular configuration is added to a toml-formatted configuration file version_added: "2.4" description: - This module will add configuration to a toml-formatted configuration file. options: dest: description: - The file to modify. required: true aliases: [ name, destfile ] json: description: - The configuration in json format to apply. Either C(json) or C(toml) has to be present. required: false default: '{}' toml: description: - The configuration in toml format to apply. Either C(json) or C(toml) has to be present. default: '' merge: description: - Used with C(state=present). If specified, it will merge the configuration. Othwerwise the configuration will be overwritten. required: false choices: [ "yes", "no" ] default: "yes" state: description: - Whether the configuration should be there or not. required: false choices: [ present, absent ] default: "present" create: description: - Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing. required: false choices: [ "yes", "no" ] default: "no" backup: description: - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. required: false choices: [ "yes", "no" ] default: "no" others: description: - All arguments accepted by the M(file) module also work here. required: false extends_documentation_fragment: - files - validate author: - "Greg Szabo (@greg-szabo)" ''' EXAMPLES = ''' # Add a new section to a toml file - name: Add comment section tomlconfig: dest: /etc/config.toml json: '{ "comment": { "comment1": "mycomment" } }' # Rewrite a toml file with the configuration - name: Create or overwrite config.toml tomlconfig: dest: /etc/config.toml json: '{ "regedit": { "freshfile": true } }' merge: no create: yes ''' RETURN = ''' changed: description: True if the configuration changed. type: bool msg: description: Description of the change type: str ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import b from ansible.module_utils._text import to_bytes, to_native import tempfile import toml as pytoml import json import copy import os def write_changes(module, b_lines, dest): tmpfd, tmpfile = tempfile.mkstemp() f = os.fdopen(tmpfd, 'wb') f.writelines(b_lines) f.close() validate = module.params.get('validate', None) valid = not validate if validate: if "%s" not in validate: module.fail_json(msg="validate must contain %%s: %s" % (validate)) (rc, out, err) = module.run_command(to_bytes(validate % tmpfile, errors='surrogate_or_strict')) valid = rc == 0 if rc != 0: module.fail_json(msg='failed to validate: ' 'rc:%s error:%s' % (rc, err)) if valid: module.atomic_move(tmpfile, to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'), unsafe_writes=module.params['unsafe_writes']) def check_file_attrs(module, changed, message, diff): file_args = module.load_file_common_arguments(module.params) if module.set_fs_attributes_if_different(file_args, False, diff=diff): if changed: message += " and " changed = True message += "ownership, perms or SE linux context changed" return message, changed #Merge dict d2 into dict d1 and return a new object def deepmerge(d1, d2): if d1 is None: return copy.deepcopy(d2) if d2 is None: return copy.deepcopy(d1) if d1 == d2: return copy.deepcopy(d1) if isinstance(d1, dict) and isinstance(d2, dict): result={} for key in set(d1.keys()+d2.keys()): da = db = None if key in d1: da = d1[key] if key in d2: db = d2[key] result[key] = deepmerge(da, db) return result else: return copy.deepcopy(d2) #Remove dict d2 from dict d1 and return a new object def deepdiff(d1, d2): if d1 is None or d2 is None: return None if d1 == d2: return None if isinstance(d1, dict) and isinstance(d2, dict): result = {} for key in d1.keys(): if key in d2: dd = deepdiff(d1[key],d2[key]) if dd is not None: result[key] = dd else: result[key] = d1[key] return result else: return None def present(module, dest, conf, jsonbool, merge, create, backup): diff = {'before': '', 'after': '', 'before_header': '%s (content)' % dest, 'after_header': '%s (content)' % dest} b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): if not create: module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) b_destpath = os.path.dirname(b_dest) if not os.path.exists(b_destpath) and not module.check_mode: os.makedirs(b_destpath) b_lines = [] else: f = open(b_dest, 'rb') b_lines = f.readlines() f.close() lines = to_native(b('').join(b_lines)) if module._diff: diff['before'] = lines b_conf = to_bytes(conf, errors='surrogate_or_strict') tomlconfig = pytoml.loads(lines) config = {} if jsonbool: config = eval(b_conf) else: config = pytoml.loads(b_conf) if not isinstance(config, dict): if jsonbool: module.fail_json(msg="Invalid value in json parameter: {0}".format(config)) else: module.fail_json(msg="Invalid value in toml parameter: {0}".format(config)) b_lines_new = b_lines msg = '' changed = False if not merge: if tomlconfig != config: b_lines_new = to_bytes(pytoml.dumps(config)) msg = 'config overwritten' changed = True else: mergedconfig = deepmerge(tomlconfig,config) if tomlconfig != mergedconfig: b_lines_new = to_bytes(pytoml.dumps(mergedconfig)) msg = 'config merged' changed = True if module._diff: diff['after'] = to_native(b('').join(b_lines_new)) backupdest = "" if changed and not module.check_mode: if backup and os.path.exists(b_dest): backupdest = module.backup_local(dest) write_changes(module, b_lines_new, dest) if module.check_mode and not os.path.exists(b_dest): module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=diff) attr_diff = {} msg, changed = check_file_attrs(module, changed, msg, attr_diff) attr_diff['before_header'] = '%s (file attributes)' % dest attr_diff['after_header'] = '%s (file attributes)' % dest difflist = [diff, attr_diff] module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) def absent(module, dest, conf, jsonbool, backup): b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): module.exit_json(changed=False, msg="file not present") msg = '' diff = {'before': '', 'after': '', 'before_header': '%s (content)' % dest, 'after_header': '%s (content)' % dest} f = open(b_dest, 'rb') b_lines = f.readlines() f.close() lines = to_native(b('').join(b_lines)) b_conf = to_bytes(conf, errors='surrogate_or_strict') lines = to_native(b('').join(b_lines)) tomlconfig = pytoml.loads(lines) config = {} if jsonbool: config = eval(b_conf) else: config = pytoml.loads(b_conf) if not isinstance(config, dict): if jsonbool: module.fail_json(msg="Invalid value in json parameter: {0}".format(config)) else: module.fail_json(msg="Invalid value in toml parameter: {0}".format(config)) if module._diff: diff['before'] = to_native(b('').join(b_lines)) b_lines_new = b_lines msg = '' changed = False diffconfig = deepdiff(tomlconfig,config) if diffconfig is None: diffconfig = {} if tomlconfig != diffconfig: b_lines_new = to_bytes(pytoml.dumps(diffconfig)) msg = 'config removed' changed = True if module._diff: diff['after'] = to_native(b('').join(b_lines_new)) backupdest = "" if changed and not module.check_mode: if backup: backupdest = module.backup_local(dest) write_changes(module, b_lines_new, dest) attr_diff = {} msg, changed = check_file_attrs(module, changed, msg, attr_diff) attr_diff['before_header'] = '%s (file attributes)' % dest attr_diff['after_header'] = '%s (file attributes)' % dest difflist = [diff, attr_diff] module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) def main(): # define the available arguments/parameters that a user can pass to # the module module_args = dict( dest=dict(type='str', required=True), json=dict(default=None), toml=dict(default=None), merge=dict(type='bool', default=True), state=dict(default='present', choices=['absent', 'present']), create=dict(type='bool', default=False), backup=dict(type='bool', default=False), validate=dict(default=None, type='str') ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule( argument_spec=module_args, mutually_exclusive=[['json', 'toml']], add_file_common_args=True, supports_check_mode=True ) params = module.params create = params['create'] merge = params['merge'] backup = params['backup'] dest = params['dest'] b_dest = to_bytes(dest, errors='surrogate_or_strict') if os.path.isdir(b_dest): module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) par_json, par_toml, jsonbool = params['json'], params['toml'], False if par_json is None: conf = par_toml else: conf = par_json jsonbool = True if params['state'] == 'present': present(module, dest, conf, jsonbool, merge, create, backup) else: absent(module, dest, conf, jsonbool, backup) if __name__ == '__main__': main()