From c35a562345ea9357a458178286a376107497774a Mon Sep 17 00:00:00 2001 From: Logan V Date: Thu, 30 Nov 2017 14:49:12 -0600 Subject: [PATCH] Add idempotency and import/export support to zabbix_template (#33362) * Add idempotency and import/export support to zabbix_template Adds idempotency to the template update functions and check mode, also adds the ability to dump and import json template configurations. * Fix issue clearing groups from template When an empty list is provided for group names, all groups associations should be cleared from the template. Previous behavior caused the template to be associated to all existing groups if an empty list was provided. * Fix undefined variable references * Add example importing template from ansible variable Document a sample template import with bare minimum structure. No items or graphs are added, only 1 application is added to the template. --- .../monitoring/zabbix/zabbix_template.py | 361 +++++++++++++++--- 1 file changed, 316 insertions(+), 45 deletions(-) diff --git a/lib/ansible/modules/monitoring/zabbix/zabbix_template.py b/lib/ansible/modules/monitoring/zabbix/zabbix_template.py index 7946f76ccc5..775bb7f75be 100644 --- a/lib/ansible/modules/monitoring/zabbix/zabbix_template.py +++ b/lib/ansible/modules/monitoring/zabbix/zabbix_template.py @@ -28,12 +28,13 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' module: zabbix_template -short_description: create/delete zabbix template +short_description: create/delete/dump zabbix template description: - - create/delete zabbix template + - create/delete/dump zabbix template version_added: "2.5" author: - "@sookido" + - "Logan Vig (@logan2211)" requirements: - "python >= 2.6" - "zabbix-api >= 0.5.3" @@ -42,10 +43,14 @@ options: description: - Name of zabbix template required: true + template_json: + description: + - JSON dump of template to import + required: false template_groups: description: - List of template groups to create or delete. - required: true + required: false link_templates: description: - List of templates linked to the template. @@ -63,7 +68,7 @@ options: description: - state present create/update template, absent delete template required: false - choices: [ present, absent] + choices: [present, absent, dump] default: "present" extends_documentation_fragment: @@ -72,13 +77,15 @@ extends_documentation_fragment: EXAMPLES = ''' --- -- name: create templates +# Creates a new zabbix template from linked template +- name: Create Zabbix template using linked template local_action: module: zabbix_template server_url: http://127.0.0.1 login_user: username login_password: password template_name: ExampleHost + template_json: "{'zabbix_export': {}}" template_groups: - Role - Role2 @@ -96,16 +103,114 @@ EXAMPLES = ''' - macro: '{$EXAMPLE_MACRO3}' value: 'Example' state: present + +# Create a new template from a json config definition +- name: Import Zabbix json template configuration + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_name: Apache2 + template_json: "{{ lookup('file', 'zabbix_apache2.json') }}" + template_groups: + - Webservers + state: present + +# Import a template from Ansible variable dict +- name: Import Zabbix Template + zabbix_template: + login_user: username + login_password: password + server_url: http://127.0.0.1 + template_name: Test Template + template_json: + zabbix_export: + version: '3.2' + templates: + - name: Template for Testing + description: 'Testing template import' + template: Test Template + groups: + - name: Templates + applications: + - name: Test Application + template_groups: Templates + state: present + +# Add a macro to a template +- name: Set a macro on the Zabbix template + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_name: Template + macros: + - macro: '{$TEST_MACRO}' + value: 'Example' + state: present + +# Remove a template +- name: Delete Zabbix template + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_name: Template + state: absent + +# Export template json definition +- name: Dump Zabbix template + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_name: Template + state: dump + register: template_dump ''' RETURN = ''' -#defaults +template_json: + description: The JSON dump of the template + returned: when state is dump + type: string + sample: { + "zabbix_export":{ + "date":"2017-11-29T16:37:24Z", + "templates":[{ + "templates":[], + "description":"", + "httptests":[], + "screens":[], + "applications":[], + "discovery_rules":[], + "groups":[{"name":"Templates"}], + "name":"Test Template", + "items":[], + "macros":[], + "template":"test" + }], + "version":"3.2", + "groups":[{ + "name":"Templates" + }] + } + } ''' from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import json +import traceback + + try: - from zabbix_api import ZabbixAPI + from zabbix_api import ZabbixAPI, ZabbixAPIException HAS_ZABBIX_API = True except ImportError: @@ -129,6 +234,8 @@ class Template(object): # get group ids by group names def get_group_ids_by_group_names(self, group_names): group_ids = [] + if group_names is None or len(group_names) == 0: + return group_ids if self.check_host_group_exist(group_names): group_list = self._zapi.hostgroup.get( {'output': 'extend', @@ -153,7 +260,7 @@ class Template(object): template_ids.append(template_id) return template_ids - def add_template(self, template_name, group_ids, + def add_template(self, template_name, template_json, group_ids, child_template_ids, macros): if self._module.check_mode: self._module.exit_json(changed=True) @@ -161,23 +268,162 @@ class Template(object): 'groups': group_ids, 'templates': child_template_ids, 'macros': macros}) + if template_json: + self.import_template(template_json, template_name) - def update_template(self, templateids, + def update_template(self, templateids, template_json, group_ids, child_template_ids, - clear_template_ids, macros): + clear_template_ids, macros, + existing_template_json=None): + changed = False + template_changes = {} + if group_ids is not None: + template_changes.update({'groups': group_ids}) + changed = True + if child_template_ids is not None: + template_changes.update({'templates': child_template_ids}) + changed = True + if macros is not None: + template_changes.update({'macros': macros}) + changed = True + do_import = False + if template_json: + parsed_template_json = self.load_json_template(template_json) + if self.diff_template(parsed_template_json, + existing_template_json): + do_import = True + changed = True + if self._module.check_mode: - self._module.exit_json(changed=True) - self._zapi.template.update( - {'templateid': templateids, 'groups': group_ids, - 'templates': child_template_ids, - 'templates_clear': clear_template_ids, - 'macros': macros}) + self._module.exit_json(changed=changed) + + if template_changes: + template_changes.update({ + 'templateid': templateids, + 'templates_clear': clear_template_ids + }) + self._zapi.template.update(template_changes) + + if do_import: + self.import_template(template_json, + existing_template_json['zabbix_export']['templates'][0]['template']) + + return changed def delete_template(self, templateids): if self._module.check_mode: self._module.exit_json(changed=True) self._zapi.template.delete(templateids) + def ordered_json(self, obj): + # Deep sort json dicts for comparison + if isinstance(obj, dict): + return sorted((k, self.ordered_json(v)) for k, v in obj.items()) + if isinstance(obj, list): + return sorted(self.ordered_json(x) for x in obj) + else: + return obj + + def dump_template(self, template_ids): + if self._module.check_mode: + self._module.exit_json(changed=True) + try: + dump = self._zapi.configuration.export({ + 'format': 'json', + 'options': {'templates': template_ids} + }) + return self.load_json_template(dump) + except ZabbixAPIException as e: + self._module.fail_json(msg='Unable to export template: %s' % e) + + def diff_template(self, template_json_a, template_json_b): + # Compare 2 zabbix templates and return True if they differ. + template_json_a = self.filter_template(template_json_a) + template_json_b = self.filter_template(template_json_b) + if self.ordered_json(template_json_a) == self.ordered_json(template_json_b): + return False + return True + + def filter_template(self, template_json): + # Filter the template json to contain only the keys we will update + keep_keys = set(['graphs', 'templates', 'triggers', 'value_maps']) + unwanted_keys = set(template_json['zabbix_export']) - keep_keys + for unwanted_key in unwanted_keys: + del template_json['zabbix_export'][unwanted_key] + return template_json + + def load_json_template(self, template_json): + try: + return json.loads(template_json) + except ValueError as e: + self._module.fail_json( + msg='Invalid JSON provided', + details=to_native(e), + exception=traceback.format_exc() + ) + + def import_template(self, template_json, template_name=None): + parsed_template_json = self.load_json_template(template_json) + if template_name != parsed_template_json['zabbix_export']['templates'][0]['template']: + self._module.fail_json(msg='JSON template name does not match presented name') + + try: + self._zapi.configuration.import_({ + 'format': 'json', + 'source': template_json, + 'rules': { + 'applications': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'discoveryRules': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'graphs': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'httptests': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'items': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'templates': { + 'createMissing': True, + 'updateExisting': True + }, + 'templateScreens': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'triggers': { + 'createMissing': True, + 'updateExisting': True, + 'deleteMissing': True + }, + 'valueMaps': { + 'createMissing': True, + 'updateExisting': True + } + } + }) + except ZabbixAPIException as e: + self._module.fail_json( + msg='Unable to import JSON template', + details=to_native(e), + exception=traceback.format_exc() + ) + def main(): module = AnsibleModule( @@ -190,11 +436,13 @@ def main(): default=None, no_log=True), validate_certs=dict(type='bool', required=False, default=True), template_name=dict(type='str', required=True), - template_groups=dict(type='list', required=True), + template_json=dict(type='json', required=False), + template_groups=dict(type='list', required=False), link_templates=dict(type='list', required=False), clear_templates=dict(type='list', required=False), macros=dict(type='list', required=False), - state=dict(default="present", choices=['present', 'absent']), + state=dict(default="present", choices=['present', 'absent', + 'dump']), timeout=dict(type='int', default=10) ), supports_check_mode=True @@ -212,10 +460,11 @@ def main(): http_login_password = module.params['http_login_password'] validate_certs = module.params['validate_certs'] template_name = module.params['template_name'] + template_json = module.params['template_json'] template_groups = module.params['template_groups'] link_templates = module.params['link_templates'] clear_templates = module.params['clear_templates'] - macros = module.params['macros'] + template_macros = module.params['macros'] state = module.params['state'] timeout = module.params['timeout'] @@ -226,11 +475,14 @@ def main(): zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password, validate_certs=validate_certs) zbx.login(login_user, login_password) - except Exception as e: + except ZabbixAPIException as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) template = Template(module, zbx) template_ids = template.get_template_ids([template_name]) + existing_template_json = None + if template_ids: + existing_template_json = template.dump_template(template_ids) # delete template if state == "absent": @@ -244,32 +496,51 @@ def main(): result="Successfully delete template %s" % template_name) - child_template_ids = [] - if link_templates: - child_template_ids = template.get_template_ids(link_templates) - - clear_template_ids = [] - if clear_templates: - clear_template_ids = template.get_template_ids(clear_templates) - - group_ids = template.get_group_ids_by_group_names(template_groups) - if not group_ids: - module.fail_json(msg='Template groups not found: %s' % - str(template_groups)) + elif state == "dump": + if not template_ids: + module.fail_json(msg='Template not found: %s' % template_name) + module.exit_json(changed=False, template_json=existing_template_json) + + elif state == "present": + child_template_ids = None + if link_templates is not None: + child_template_ids = template.get_template_ids(link_templates) + + clear_template_ids = [] + if clear_templates is not None: + clear_template_ids = template.get_template_ids(clear_templates) + + group_ids = None + if template_groups is not None: + # If the template exists, compare the already set groups + existing_groups = None + if existing_template_json: + existing_groups = set(list(group['name'] for group in existing_template_json['zabbix_export']['groups'])) + if not existing_groups or set(template_groups) != existing_groups: + group_ids = template.get_group_ids_by_group_names(template_groups) + + macros = None + if template_macros is not None: + existing_macros = None + if existing_template_json: + existing_macros = set(existing_template_json['zabbix_export']['templates'][0]['macros']) + if not existing_macros or set(template_macros) != existing_macros: + macros = template_macros - if not template_ids: - template.add_template(template_name, group_ids, - child_template_ids, macros) - module.exit_json(changed=True, - result="Successfully added template: %s" % - template_name) - else: - template.update_template(template_ids[0], group_ids, - child_template_ids, clear_template_ids, - macros) - module.exit_json(changed=True, - result="Successfully updateed template: %s" % - template_name) + if not template_ids: + template.add_template(template_name, template_json, group_ids, + child_template_ids, macros) + module.exit_json(changed=True, + result="Successfully added template: %s" % + template_name) + else: + changed = template.update_template(template_ids[0], template_json, + group_ids, child_template_ids, + clear_template_ids, macros, + existing_template_json) + module.exit_json(changed=changed, + result="Successfully updateed template: %s" % + template_name) if __name__ == '__main__':