diff --git a/changelogs/fragments/59040-zabbix_template_xml_support.yml b/changelogs/fragments/59040-zabbix_template_xml_support.yml new file mode 100644 index 00000000000..f29c08ef32c --- /dev/null +++ b/changelogs/fragments/59040-zabbix_template_xml_support.yml @@ -0,0 +1,8 @@ +minor_changes: + - zabbix_template - new parameter `template_xml` adds support for importing templates from XML documents + - zabbix_template - new parameter `dump_format` allows user to specify in which format (JSON or XML) should a template be exported from Zabbix + - zabbix_template - it is no longer accepted to provide parameters `template_name` and `template_groups` when using `template_json` + - zabbix_template - parameters `template_name`, `template_json` and `template_xml` are now mutually exclusive + - zabbix_template - parameter `template_groups` is now required when passing `template_name` and template is being created for the fisrt time. Not required when template is being updated. + - zabbix_template - now allows import of multiple templates at once when using `template_json` or `template_xml` parameters + - zabbix_template - template can now be updated with just a `clear_templates` parameter without requiring any additional parameters to be passed (see examples of the module) diff --git a/lib/ansible/modules/monitoring/zabbix/zabbix_template.py b/lib/ansible/modules/monitoring/zabbix/zabbix_template.py index fe4d4910656..59f2b3ebae7 100644 --- a/lib/ansible/modules/monitoring/zabbix/zabbix_template.py +++ b/lib/ansible/modules/monitoring/zabbix/zabbix_template.py @@ -16,13 +16,15 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: zabbix_template -short_description: Create/delete/dump Zabbix template +short_description: Create/update/delete/dump Zabbix template description: - - Create/delete/dump Zabbix template. + - This module allows you to create, modify, delete and dump Zabbix templates. + - Multiple templates can be created or modified at once if passing JSON or XML to module. version_added: "2.5" author: - "sookido (@sookido)" - "Logan Vig (@logan2211)" + - "Dusan Matejka (@D3DeFi)" requirements: - "python >= 2.6" - "zabbix-api >= 0.5.3" @@ -30,33 +32,75 @@ options: template_name: description: - Name of Zabbix template. - - Required when I(template_json) is not used. + - Required when I(template_json) or I(template_xml) are not used. + - Mutually exclusive with I(template_json) and I(template_xml). required: false template_json: description: - - JSON dump of template to import. + - JSON dump of templates to import. + - Multiple templates can be imported this way. + - Mutually exclusive with I(template_name) and I(template_xml). required: false + type: json + template_xml: + description: + - XML dump of templates to import. + - Multiple templates can be imported this way. + - You are advised to pass XML structure matching the structure used by your version of Zabbix server. + - Custom XML structure can be imported as long as it is valid, but may not yield consistent idempotent + results on subsequent runs. + - Mutually exclusive with I(template_name) and I(template_json). + required: false + version_added: '2.9' template_groups: description: - - List of template groups to create or delete. - - Required when I(template_name) is used and C(state=present). + - List of host groups to add template to when template is createad. + - Replaces the current host groups the template belongs to if the template is already present. + - Required when creating a new template with C(state=present) and I(template_name) is used. + Not required when updating an existing template. required: false + type: list link_templates: description: - - List of templates linked to the template. + - List of template names to be linked to the template. + - Templates that are not specified and are linked to the existing template will be only unlinked and not + cleared from the template. required: false + type: list clear_templates: description: - - List of templates cleared from the template. - - See templates_clear in https://www.zabbix.com/documentation/3.0/manual/api/reference/template/update + - List of template names to be unlinked and cleared from the template. + - This option is ignored if template is being created for the first time. required: false + type: list macros: description: - - List of template macros. + - List of user macros to create for the template. + - Macros that are not specified and are present on the existing template will be replaced. + - See examples on how to pass macros. required: false + type: list + suboptions: + name: + description: + - Name of the macro. + - Must be specified in {$NAME} format. + value: + description: + - Value of the macro. + dump_format: + description: + - Format to use when dumping template with C(state=dump). + required: false + choices: [json, xml] + default: "json" + version_added: '2.9' state: description: - - 'State: present - create/update template; absent - delete template' + - Required state of the template. + - On C(state=present) template will be created/imported or updated depending if it is already present. + - On C(state=dump) template content will get dumped into required format specified in I(dump_format). + - On C(state=absent) template will be deleted. required: false choices: [present, absent, dump] default: "present" @@ -67,24 +111,19 @@ extends_documentation_fragment: EXAMPLES = ''' --- -# Creates a new Zabbix template from linked template -- name: Create Zabbix template using linked template +- name: Create a new Zabbix template linked to groups, macros and templates 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 link_templates: - Example template1 - Example template2 - clear_templates: - - Example template3 - - Example template4 macros: - macro: '{$EXAMPLE_MACRO1}' value: 30000 @@ -94,26 +133,41 @@ EXAMPLES = ''' value: 'Example' state: present -# Create a new template from a JSON config definition -- name: Import Zabbix JSON template configuration +- name: Unlink and clear templates from the existing Zabbix template 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 + template_name: ExampleHost + clear_templates: + - Example template3 + - Example template4 state: present -# Import a template from Ansible variable dict -- name: Import Zabbix template +- name: Import Zabbix templates from JSON + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_json: "{{ lookup('file', 'zabbix_apache2.json') }}" + state: present + +- name: Import Zabbix templates from XML + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_xml: "{{ lookup('file', 'zabbix_apache2.json') }}" + state: present + +- name: Import Zabbix template from Ansible dict variable 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' @@ -125,11 +179,9 @@ EXAMPLES = ''' - 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 +- name: Configure macros on the existing Zabbix template local_action: module: zabbix_template server_url: http://127.0.0.1 @@ -141,7 +193,6 @@ EXAMPLES = ''' value: 'Example' state: present -# Remove a template - name: Delete Zabbix template local_action: module: zabbix_template @@ -151,8 +202,7 @@ EXAMPLES = ''' template_name: Template state: absent -# Export template JSON definition -- name: Dump Zabbix template +- name: Dump Zabbix template as JSON local_action: module: zabbix_template server_url: http://127.0.0.1 @@ -161,6 +211,17 @@ EXAMPLES = ''' template_name: Template state: dump register: template_dump + +- name: Dump Zabbix template as XML + local_action: + module: zabbix_template + server_url: http://127.0.0.1 + login_user: username + login_password: password + template_name: Template + dump_format: xml + state: dump + register: template_dump ''' RETURN = ''' @@ -191,15 +252,53 @@ template_json: }] } } + +template_xml: + description: dump of the template in XML representation + returned: when state is dump and dump_format is xml + type: str + sample: |- + + + 4.2 + 2019-07-12T13:37:26Z + + + Templates + + + + + + ''' + +import atexit +import json +import traceback +import xml.etree.ElementTree as ET + from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native -import atexit -import json -import traceback - try: from zabbix_api import ZabbixAPI, ZabbixAPIException @@ -253,58 +352,87 @@ class Template(object): template_ids.append(template_id) return template_ids - def add_template(self, template_name, template_json, group_ids, - child_template_ids, macros): + def add_template(self, template_name, group_ids, link_template_ids, macros): if self._module.check_mode: self._module.exit_json(changed=True) - if template_json: - self.import_template(template_json, template_name) - else: - self._zapi.template.create({'host': template_name, - 'groups': group_ids, - 'templates': child_template_ids, - 'macros': macros}) + self._zapi.template.create({'host': template_name, 'groups': group_ids, 'templates': link_template_ids, + 'macros': macros}) - def update_template(self, templateids, template_json, - group_ids, child_template_ids, - clear_template_ids, macros, - existing_template_json=None): + def check_template_changed(self, template_ids, template_groups, link_templates, clear_templates, + template_macros, template_content, template_type): + """Compares template parameters to already existing values if any are found. + + template_json - JSON structures are compared as deep sorted dictonaries, + template_xml - XML structures are compared as strings, but filtered and formatted first, + If none above is used, all the other arguments are compared to their existing counterparts + retrieved from Zabbix API.""" changed = False + # Compare filtered and formatted XMLs strings for any changes. It is expected that provided + # XML has same structure as Zabbix uses (e.g. it was optimally exported via Zabbix GUI or API) + if template_content is not None and template_type == 'xml': + existing_template = self.dump_template(template_ids, template_type='xml') + + if self.filter_xml_template(template_content) != self.filter_xml_template(existing_template): + changed = True + + return changed + + existing_template = self.dump_template(template_ids, template_type='json') + # Compare JSON objects as deep sorted python dictonaries + if template_content is not None and template_type == 'json': + parsed_template_json = self.load_json_template(template_content) + if self.diff_template(parsed_template_json, existing_template): + changed = True + + return changed + + # If neither template_json or template_xml were used, user provided all parameters via module options + if template_groups is not None: + existing_groups = [g['name'] for g in existing_template['zabbix_export']['groups']] + + if set(template_groups) != set(existing_groups): + changed = True + + # Check if any new templates would be linked or any existing would be unlinked + exist_child_templates = [t['name'] for t in existing_template['zabbix_export']['templates'][0]['templates']] + if link_templates is not None: + if set(link_templates) != set(exist_child_templates): + changed = True + + # Mark that there will be changes when at least one existing template will be unlinked + if clear_templates is not None: + for t in clear_templates: + if t in exist_child_templates: + changed = True + break + + if template_macros is not None: + existing_macros = existing_template['zabbix_export']['templates'][0]['macros'] + if template_macros != existing_macros: + changed = True + + return changed + + def update_template(self, template_ids, group_ids, link_template_ids, clear_template_ids, template_macros): 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=changed) + if link_template_ids is not None: + template_changes.update({'templates': link_template_ids}) + + if clear_template_ids is not None: + template_changes.update({'templates_clear': clear_template_ids}) + + if template_macros is not None: + template_changes.update({'macros': template_macros}) if template_changes: - template_changes.update({ - 'templateid': templateids, - 'templates_clear': clear_template_ids - }) + # If we got here we know that only one template was provided via template_name + template_changes.update({'templateid': template_ids[0]}) 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) @@ -319,15 +447,17 @@ class Template(object): else: return obj - def dump_template(self, template_ids): + def dump_template(self, template_ids, template_type='json'): 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) + dump = self._zapi.configuration.export({'format': template_type, 'options': {'templates': template_ids}}) + if template_type == 'xml': + return str(ET.tostring(ET.fromstring(dump.encode('utf-8')), encoding='utf-8')) + else: + return self.load_json_template(dump) + except ZabbixAPIException as e: self._module.fail_json(msg='Unable to export template: %s' % e) @@ -359,21 +489,43 @@ class Template(object): return template_json + def filter_xml_template(self, template_xml): + """Filters out keys from XML template that may wary between exports (e.g date or version) and + keys that are not imported via this module. + + It is advised that provided XML template exactly matches XML structure used by Zabbix""" + # Strip last new line and convert string to ElementTree + parsed_xml_root = self.load_xml_template(template_xml.strip()) + keep_keys = ['graphs', 'templates', 'triggers', 'value_maps'] + + # Remove unwanted XML nodes + for node in list(parsed_xml_root): + if node.tag not in keep_keys: + parsed_xml_root.remove(node) + + # Filter empty attributes from template objects to allow accurate comparison + for template in list(parsed_xml_root.find('templates')): + for element in list(template): + if element.text is None and len(list(element)) == 0: + template.remove(element) + + # Filter new lines and identation + xml_root_text = list(line.strip() for line in ET.tostring(parsed_xml_root).split('\n')) + return ''.join(xml_root_text) + 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() - ) + 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') + def load_xml_template(self, template_xml): + try: + return ET.fromstring(template_xml) + except ET.ParseError as e: + self._module.fail_json(msg='Invalid XML provided', details=to_native(e), exception=traceback.format_exc()) + def import_template(self, template_content, template_type='json'): # rules schema latest version update_rules = { 'applications': { @@ -427,21 +579,14 @@ class Template(object): # old api version support here api_version = self._zapi.api_version() # updateExisting for application removed from zabbix api after 3.2 - if LooseVersion(api_version).version[:2] <= LooseVersion( - '3.2').version: + if LooseVersion(api_version).version[:2] <= LooseVersion('3.2').version: update_rules['applications']['updateExisting'] = True - self._zapi.configuration.import_({ - 'format': 'json', - 'source': template_json, - 'rules': update_rules - }) + import_data = {'format': template_type, 'source': template_content, 'rules': update_rules} + self._zapi.configuration.import_(import_data) except ZabbixAPIException as e: - self._module.fail_json( - msg='Unable to import JSON template', - details=to_native(e), - exception=traceback.format_exc() - ) + self._module.fail_json(msg='Unable to import template', details=to_native(e), + exception=traceback.format_exc()) def main(): @@ -451,20 +596,29 @@ def main(): login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), http_login_user=dict(type='str', required=False, default=None), - http_login_password=dict(type='str', required=False, - default=None, no_log=True), + http_login_password=dict(type='str', required=False, default=None, no_log=True), validate_certs=dict(type='bool', required=False, default=True), template_name=dict(type='str', required=False), template_json=dict(type='json', required=False), + template_xml=dict(type='str', 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', - 'dump']), + dump_format=dict(type='str', required=False, default='json', choices=['json', 'xml']), + state=dict(default="present", choices=['present', 'absent', 'dump']), timeout=dict(type='int', default=10) ), - required_one_of=[['template_name', 'template_json']], + required_one_of=[ + ['template_name', 'template_json', 'template_xml'] + ], + mutually_exclusive=[ + ['template_name', 'template_json', 'template_xml'] + ], + required_if=[ + ['state', 'absent', ['template_name']], + ['state', 'dump', ['template_name']] + ], supports_check_mode=True ) @@ -479,104 +633,110 @@ def main(): validate_certs = module.params['validate_certs'] template_name = module.params['template_name'] template_json = module.params['template_json'] + template_xml = module.params['template_xml'] template_groups = module.params['template_groups'] link_templates = module.params['link_templates'] clear_templates = module.params['clear_templates'] template_macros = module.params['macros'] + dump_format = module.params['dump_format'] state = module.params['state'] timeout = module.params['timeout'] zbx = None - - # login to zabbix try: - zbx = ZabbixAPI(server_url, timeout=timeout, - user=http_login_user, passwd=http_login_password, validate_certs=validate_certs) + zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password, + validate_certs=validate_certs) zbx.login(login_user, login_password) atexit.register(zbx.logout) except ZabbixAPIException as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) template = Template(module, zbx) - if template_json and not template_name: - # Ensure template_name is not empty for safety check in import_template - template_loaded = template.load_json_template(template_json) - template_name = template_loaded['zabbix_export']['templates'][0]['template'] - elif template_name and not template_groups and state == 'present': - module.fail_json(msg="Option template_groups is required when template_json is not used") - template_ids = template.get_template_ids([template_name]) - existing_template_json = None - if template_ids: - existing_template_json = template.dump_template(template_ids) + # Identify template names for IDs retrieval + # Template names are expected to reside in ['zabbix_export']['templates'][*]['template'] for both data types + template_content, template_type = None, None + if template_json is not None: + template_type = 'json' + template_content = template_json + json_parsed = template.load_json_template(template_content) + template_names = list(t['template'] for t in json_parsed['zabbix_export']['templates']) + + elif template_xml is not None: + template_type = 'xml' + template_content = template_xml + xml_parsed = template.load_xml_template(template_content) + template_names = list(t.find('template').text for t in list(xml_parsed.find('templates'))) + + else: + template_names = [template_name] + + template_ids = template.get_template_ids(template_names) - # delete template if state == "absent": - # if template not found. no change, no fail if not template_ids: - module.exit_json(changed=False, - msg="Template not found. " + - "No changed: %s" % template_name) + module.exit_json(changed=False, msg="Template not found. No changed: %s" % template_name) + template.delete_template(template_ids) - module.exit_json(changed=True, - result="Successfully delete template %s" % - template_name) + module.exit_json(changed=True, result="Successfully deleted template %s" % template_name) 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) + + if dump_format == 'json': + module.exit_json(changed=False, template_json=template.dump_template(template_ids, template_type='json')) + elif dump_format == 'xml': + module.exit_json(changed=False, template_xml=template.dump_template(template_ids, template_type='xml')) elif state == "present": - child_template_ids = None - if link_templates is not None: - existing_child_templates = None - if existing_template_json: - existing_child_templates = set(list( - tmpl['name'] for tmpl in existing_template_json['zabbix_export']['templates'][0]['templates'])) - if not existing_template_json or set(link_templates) != existing_child_templates: - child_template_ids = template.get_template_ids(link_templates) + # Load all subelements for template that were provided by user + group_ids = None + if template_groups is not None: + group_ids = template.get_group_ids_by_group_names(template_groups) - clear_template_ids = [] + link_template_ids = None + if link_templates is not None: + link_template_ids = template.get_template_ids(link_templates) + + clear_template_ids = None 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 - # zabbix configuration.export does not differentiate python types (numbers are returned as strings) + # Zabbix configuration.export does not differentiate python types (numbers are returned as strings) for macroitem in template_macros: for key in macroitem: macroitem[key] = str(macroitem[key]) - if existing_template_json: - existing_macros = existing_template_json['zabbix_export']['templates'][0]['macros'] - if not existing_macros or template_macros != existing_macros: - macros = template_macros - 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) + # Assume new templates are being added when no ID's were found + if template_content is not None: + template.import_template(template_content, template_type) + module.exit_json(changed=True, result="Template import successful") + + else: + if group_ids is None: + module.fail_json(msg='template_groups are required when creating a new Zabbix template') + + template.add_template(template_name, group_ids, link_template_ids, template_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 updated template: %s" % - template_name) + changed = template.check_template_changed(template_ids, template_groups, link_templates, clear_templates, + template_macros, template_content, template_type) + + if module.check_mode: + module.exit_json(changed=changed) + + if changed: + if template_type is not None: + template.import_template(template_content, template_type) + else: + template.update_template(template_ids, group_ids, link_template_ids, clear_template_ids, + template_macros) + + module.exit_json(changed=changed, result="Template successfully updated") if __name__ == '__main__':