From 6848f8b4073b7c85a52d0448617510dd0349d5cf Mon Sep 17 00:00:00 2001 From: Greg Szabo Date: Tue, 19 Sep 2017 12:47:05 -0400 Subject: [PATCH] Introduced jsonconfig task for ansible playbooks --- ansible/jsonconfig.yml | 8 + .../roles/jsonconfig/library/jsonconfig.py | 360 ++++++++++++++++++ ansible/roles/jsonconfig/tasks/main.yml | 5 + ansible/roles/tomlconfig/defaults/main.yml | 3 + .../roles/tomlconfig/library/tomlconfig.py | 16 +- ansible/roles/tomlconfig/tasks/main.yml | 4 +- 6 files changed, 383 insertions(+), 13 deletions(-) create mode 100644 ansible/jsonconfig.yml create mode 100644 ansible/roles/jsonconfig/library/jsonconfig.py create mode 100644 ansible/roles/jsonconfig/tasks/main.yml create mode 100644 ansible/roles/tomlconfig/defaults/main.yml diff --git a/ansible/jsonconfig.yml b/ansible/jsonconfig.yml new file mode 100644 index 000000000..de1f5b459 --- /dev/null +++ b/ansible/jsonconfig.yml @@ -0,0 +1,8 @@ +--- + +#variable "service" is required + +- hosts: "{{ lookup('env','TF_VAR_TESTNET_NAME') }}:tag_Environment_{{ lookup('env','TF_VAR_TESTNET_NAME') | regex_replace('-','_') }}" + roles: + - jsonconfig + diff --git a/ansible/roles/jsonconfig/library/jsonconfig.py b/ansible/roles/jsonconfig/library/jsonconfig.py new file mode 100644 index 000000000..d3b4140e4 --- /dev/null +++ b/ansible/roles/jsonconfig/library/jsonconfig.py @@ -0,0 +1,360 @@ +#!/usr/bin/python + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: jsonconfig + +short_description: Ensure a particular configuration is added to a json-formatted configuration file + +version_added: "2.4" + +description: + - This module will add configuration to a json-formatted configuration file. + +options: + dest: + description: + - The file to modify. + required: true + aliases: [ name, destfile ] + json: + description: + - The configuration in json format to apply. + required: false + 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 json file +- name: Add comment section + jsonconfig: + dest: /etc/something.json + json: '{ "comment": { "comment1": "mycomment" } }' + +# Rewrite a json file with the configuration +- name: Create or overwrite config.json + jsonconfig: + dest: /etc/config.json + 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 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') + + jsonconfig = json.loads(lines) + config = eval(b_conf) + + if not isinstance(config, dict): + module.fail_json(msg="Invalid value in json parameter: {0}".format(config)) + + b_lines_new = b_lines + msg = '' + changed = False + + if not merge: + if jsonconfig != config: + b_lines_new = to_bytes(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': '))) + msg = 'config overwritten' + changed = True + else: + mergedconfig = deepmerge(jsonconfig,config) + if jsonconfig != mergedconfig: + b_lines_new = to_bytes(json.dumps(mergedconfig), sort_keys=True, indent=4, separators=(',', ': '))) + 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)) + jsonconfig = json.loads(lines) + config = eval(b_conf) + + if not isinstance(config, dict): + module.fail_json(msg="Invalid value in json 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(jsonconfig,config) + if diffconfig is None: + diffconfig = {} + if jsonconfig != diffconfig: + b_lines_new = to_bytes(json.dumps(diffconfig, sort_keys=True, indent=4, separators=(',', ': '))) + 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, required=True), + 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, + 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) + + conf = params['json'] + + 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/jsonconfig/tasks/main.yml b/ansible/roles/jsonconfig/tasks/main.yml new file mode 100644 index 000000000..277be1095 --- /dev/null +++ b/ansible/roles/jsonconfig/tasks/main.yml @@ -0,0 +1,5 @@ +--- + +- name: Update + jsonconfig: "dest='{{destination}}' json='{{jsonconfig}}' state={{(remove | default(false) | bool) | ternary('absent','present')}}" + diff --git a/ansible/roles/tomlconfig/defaults/main.yml b/ansible/roles/tomlconfig/defaults/main.yml new file mode 100644 index 000000000..236d6eb80 --- /dev/null +++ b/ansible/roles/tomlconfig/defaults/main.yml @@ -0,0 +1,3 @@ +--- +destination: /etc/{{service}}/tendermint/config.toml + diff --git a/ansible/roles/tomlconfig/library/tomlconfig.py b/ansible/roles/tomlconfig/library/tomlconfig.py index ced62d07a..fbb10b823 100644 --- a/ansible/roles/tomlconfig/library/tomlconfig.py +++ b/ansible/roles/tomlconfig/library/tomlconfig.py @@ -88,21 +88,15 @@ EXAMPLES = ''' 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 +changed: + description: True if the configuration changed. type: bool +msg: + description: Description of the change + type: str ''' from ansible.module_utils.basic import AnsibleModule diff --git a/ansible/roles/tomlconfig/tasks/main.yml b/ansible/roles/tomlconfig/tasks/main.yml index 518e0f3ec..8fe47e821 100644 --- a/ansible/roles/tomlconfig/tasks/main.yml +++ b/ansible/roles/tomlconfig/tasks/main.yml @@ -2,9 +2,9 @@ - name: Update config.toml with json when: jsonconfig is defined - tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml json='{{jsonconfig}}' state={{(remove | default(false) | bool) | ternary('absent','present')}}" + tomlconfig: "dest='{{destination}}' json='{{jsonconfig}}' state={{(remove | default(false) | bool) | ternary('absent','present')}}" - name: Update config.toml with toml when: tomlconfig is defined - tomlconfig: "dest=/etc/{{service}}/tendermint/config.toml toml='{{tomlconfig}}' state={{(remove | default(false) | bool) | ternary('absent','present')}}" + tomlconfig: "dest='{{destination}}' toml='{{tomlconfig}}' state={{(remove | default(false) | bool) | ternary('absent','present')}}"