From 4f47a762a0e2326755798724f515546df3913d45 Mon Sep 17 00:00:00 2001 From: Greg Szabo Date: Fri, 15 Sep 2017 02:10:29 -0400 Subject: [PATCH] Introduced tomlconfig playbook --- .../roles/tomlconfig/library/tomlconfig.py | 392 ++++++++++++++++++ ansible/roles/tomlconfig/tasks/centos.yml | 17 + ansible/roles/tomlconfig/tasks/debian.yml | 6 + ansible/roles/tomlconfig/tasks/main.yml | 24 ++ ansible/tomlconfig.yml | 9 + 5 files changed, 448 insertions(+) create mode 100644 ansible/roles/tomlconfig/library/tomlconfig.py create mode 100644 ansible/roles/tomlconfig/tasks/centos.yml create mode 100644 ansible/roles/tomlconfig/tasks/debian.yml create mode 100644 ansible/roles/tomlconfig/tasks/main.yml create mode 100644 ansible/tomlconfig.yml diff --git a/ansible/roles/tomlconfig/library/tomlconfig.py b/ansible/roles/tomlconfig/library/tomlconfig.py new file mode 100644 index 000000000..ced62d07a --- /dev/null +++ b/ansible/roles/tomlconfig/library/tomlconfig.py @@ -0,0 +1,392 @@ +#!/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 + +- name: Set file permissions + tomlconfig: + dest: /etc/config.toml + mode: 0600 + owner: root + group: root +''' + +RETURN = ''' +config: + description: The resultant configuration + type: str +created: + description: True if the file was freshly created + type: bool +''' + +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() + diff --git a/ansible/roles/tomlconfig/tasks/centos.yml b/ansible/roles/tomlconfig/tasks/centos.yml new file mode 100644 index 000000000..f276e796d --- /dev/null +++ b/ansible/roles/tomlconfig/tasks/centos.yml @@ -0,0 +1,17 @@ +--- + +- name: Install epel-release on CentOS/RedHat + when: ansible_os_family == "RedHat" + yum: "pkg=epel-release update_cache=yes state=latest" + +- name: Install pip on CentOS/RedHat + when: ansible_os_family == "RedHat" + yum: "pkg={{item}} state=latest" + with_items: + - python2-pip + - python-virtualenv + +- name: Install toml + when: ansible_os_family == "RedHat" + pip: name=toml + diff --git a/ansible/roles/tomlconfig/tasks/debian.yml b/ansible/roles/tomlconfig/tasks/debian.yml new file mode 100644 index 000000000..f5e287deb --- /dev/null +++ b/ansible/roles/tomlconfig/tasks/debian.yml @@ -0,0 +1,6 @@ +--- + +- name: Install package on Debian/Ubuntu + when: ansible_os_family == "Debian" + apt: "pkg=python-toml state=latest" + diff --git a/ansible/roles/tomlconfig/tasks/main.yml b/ansible/roles/tomlconfig/tasks/main.yml new file mode 100644 index 000000000..06132b14a --- /dev/null +++ b/ansible/roles/tomlconfig/tasks/main.yml @@ -0,0 +1,24 @@ +--- + +- include: debian.yml + when: ansible_os_family == "Debian" + +- include: centos.yml + when: ansible_os_family == "RedHat" + +- name: Update config.toml with json + when: jsonconfig is defined + tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml json='{{jsonconfig}}'" + +- name: Update config.toml with toml + when: tomlconfig is defined + tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml toml='{{tomlconfig}}'" + +- name: Update config.toml with json - removal + when: jsonconfigremove is defined + tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml json='{{jsonconfigremove}}'" + +- name: Update config.toml with toml - removal + when: tomlconfigremove is defined + tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml toml='{{tomlconfigremove}}'" + diff --git a/ansible/tomlconfig.yml b/ansible/tomlconfig.yml new file mode 100644 index 000000000..e6cdd33f5 --- /dev/null +++ b/ansible/tomlconfig.yml @@ -0,0 +1,9 @@ +--- + +#variable "service" is required + +- hosts: "{{ lookup('env','TF_VAR_TESTNET_NAME') }}:tag_Environment_{{ lookup('env','TF_VAR_TESTNET_NAME') | regex_replace('-','_') }}" + any_errors_fatal: "{{validators | default(true) | bool}}" + roles: + - tomlconfig +