From 7eebec59b3ed5c26f723cabe8007a0a8eeb908ad Mon Sep 17 00:00:00 2001 From: Anton Alekseyev Date: Sun, 18 Nov 2018 21:57:51 +0700 Subject: [PATCH] zabbix_map: Add module to create Zabbix maps based on data written in DOT language (#23026) * Add zabbix_map module * Fix PEP8 complainments * Fix dict comprehension incompatible with python 2.6 * Support Zabbix 3.4 API changes * Fix documentation * Minor fixes * Move zabbix_map to zabbix namespace * Fix compatibility issue with Zabbix >= 3.4 * Support maps and triggers as map elements --- .../modules/monitoring/zabbix/zabbix_map.py | 812 ++++++++++++++++++ 1 file changed, 812 insertions(+) create mode 100644 lib/ansible/modules/monitoring/zabbix/zabbix_map.py diff --git a/lib/ansible/modules/monitoring/zabbix/zabbix_map.py b/lib/ansible/modules/monitoring/zabbix/zabbix_map.py new file mode 100644 index 0000000000..6f6210899b --- /dev/null +++ b/lib/ansible/modules/monitoring/zabbix/zabbix_map.py @@ -0,0 +1,812 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017-2018, Antony Alekseyev +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: zabbix_map +author: + - "Antony Alekseyev (@Akint)" +short_description: Zabbix map creates/updates/deletes +description: + - "This module allows you to create, modify and delete Zabbix map entries, + using Graphviz binaries and text description written in DOT language. + Nodes of the graph will become map elements and edges will become links between map elements. + See U(https://en.wikipedia.org/wiki/DOT_(graph_description_language)) and U(https://www.graphviz.org/) for details. + Inspired by U(http://blog.zabbix.com/maps-for-the-lazy/)." + - "The following extra node attributes are supported: + C(zbx_host) contains name of the host in Zabbix. Use this if desired type of map element is C(host). + C(zbx_group) contains name of the host group in Zabbix. Use this if desired type of map element is C(host group). + C(zbx_map) contains name of the map in Zabbix. Use this if desired type of map element is C(map). + C(zbx_label) contains label of map element. + C(zbx_image) contains name of the image used to display the element in default state. + C(zbx_image_disabled) contains name of the image used to display disabled map element. + C(zbx_image_maintenance) contains name of the image used to display map element in maintenance. + C(zbx_image_problem) contains name of the image used to display map element with problems. + C(zbx_url) contains map element URL in C(name:url) format. + More than one url could be specified by adding some prefix (e.g., C(zbx_url1), C(zbx_url2))." + - "The following extra link attributes are supported: + C(zbx_draw_style) contains link line draw style. Possible values: C(line), C(bold), C(dotted), C(dashed). + C(zbx_trigger) contains name of the trigger used as a link indicator in C(host_name:trigger_name) format. + More than one trigger could be specified by adding some prefix (e.g., C(zbx_trigger1), C(zbx_trigger2)). + C(zbx_trigger_color) contains indicator color specified either as CSS3 name or as a hexadecimal code starting with C(#). + C(zbx_trigger_draw_style) contains indicator draw style. Possible values are the same as for C(zbx_draw_style)." +requirements: + - "python >= 2.6" + - zabbix-api + - pydotplus + - webcolors + - Pillow + - Graphviz +version_added: "2.8" +options: + name: + description: + - Name of the map. + required: true + aliases: [ "map_name" ] + data: + description: + - Graph written in DOT language. + required: false + aliases: [ "dot_data" ] + state: + description: + - State of the map. + - On C(present), it will create if map does not exist or update the map if the associated data is different. + - On C(absent) will remove the map if it exists. + required: false + choices: ['present', 'absent'] + default: "present" + width: + description: + - Width of the map. + required: false + default: 800 + height: + description: + - Height of the map. + required: false + default: 600 + margin: + description: + - Size of white space between map's borders and its elements. + required: false + default: 40 + expand_problem: + description: + - Whether the the problem trigger will be displayed for elements with a single problem. + required: false + type: bool + default: true + highlight: + description: + - Whether icon highlighting is enabled. + required: false + type: bool + default: true + label_type: + description: + - Map element label type. + required: false + choices: ['label', 'ip', 'name', 'status', 'nothing', 'custom'] + default: "name" + default_image: + description: + - Name of the Zabbix image used to display the element if this element doesn't have the C(zbx_image) attribute defined. + required: false + aliases: [ "image" ] + +extends_documentation_fragment: + - zabbix +''' + +RETURN = ''' # ''' + +EXAMPLES = ''' +### +### Example inventory: +# [web] +# web[01:03].example.com ansible_host=127.0.0.1 +# [db] +# db.example.com ansible_host=127.0.0.1 +# [backup] +# backup.example.com ansible_host=127.0.0.1 +### +### Each inventory host presents in Zabbix with same name. +### +### Contents of 'map.j2': +# digraph G { +# graph [layout=dot splines=false overlap=scale] +# INTERNET [zbx_url="Google:https://google.com" zbx_image="Cloud_(96)"] +# {% for web_host in groups.web %} +# {% set web_loop = loop %} +# web{{ '%03d' % web_loop.index }} [zbx_host="{{ web_host }}"] +# INTERNET -> web{{ '%03d' % web_loop.index }} [zbx_trigger="{{ web_host }}:Zabbix agent on {HOST.NAME} is unreachable for 5 minutes"] +# {% for db_host in groups.db %} +# {% set db_loop = loop %} +# web{{ '%03d' % web_loop.index }} -> db{{ '%03d' % db_loop.index }} +# {% endfor %} +# {% endfor %} +# { rank=same +# {% for db_host in groups.db %} +# {% set db_loop = loop %} +# db{{ '%03d' % db_loop.index }} [zbx_host="{{ db_host }}"] +# {% for backup_host in groups.backup %} +# {% set backup_loop = loop %} +# db{{ '%03d' % db_loop.index }} -> backup{{ '%03d' % backup_loop.index }} [color="blue"] +# {% endfor %} +# {% endfor %} +# {% for backup_host in groups.backup %} +# {% set backup_loop = loop %} +# backup{{ '%03d' % backup_loop.index }} [zbx_host="{{ backup_host }}"] +# {% endfor %} +# } +# } +### +### Create Zabbix map "Demo Map" made of template 'map.j2' +- name: Create Zabbix map + zabbix_map: + server_url: http://zabbix.example.com + login_user: username + login_password: password + name: Demo map + state: present + data: "{{ lookup('template', 'map.j2') }}" + default_image: Server_(64) + expand_problem: no + highlight: no + label_type: label + delegate_to: localhost + run_once: yes +''' + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +import base64 +from io import BytesIO +from operator import itemgetter +from distutils.version import StrictVersion +from ansible.module_utils.basic import AnsibleModule + +try: + import pydotplus + HAS_PYDOTPLUS = True +except ImportError: + HAS_PYDOTPLUS = False + +try: + import webcolors + HAS_WEBCOLORS = True +except ImportError: + HAS_WEBCOLORS = False + +try: + from zabbix_api import ZabbixAPI, ZabbixAPISubClass + HAS_ZABBIX_API = True +except ImportError: + HAS_ZABBIX_API = False + +try: + from PIL import Image + HAS_PIL = True +except ImportError: + HAS_PIL = False + + +class Map(): + def __init__(self, module, zbx): + self._module = module + self._zapi = zbx + + self.map_name = module.params['name'] + self.dot_data = module.params['data'] + self.width = module.params['width'] + self.height = module.params['height'] + self.state = module.params['state'] + self.default_image = module.params['default_image'] + self.map_id = self._get_sysmap_id(self.map_name) + self.margin = module.params['margin'] + self.expand_problem = module.params['expand_problem'] + self.highlight = module.params['highlight'] + self.label_type = module.params['label_type'] + self.api_version = self._zapi.api_version() + self.selements_sort_keys = self._get_selements_sort_keys() + + def _build_graph(self): + try: + graph_without_positions = pydotplus.graph_from_dot_data(self.dot_data) + dot_data_with_positions = graph_without_positions.create_dot() + graph_with_positions = pydotplus.graph_from_dot_data(dot_data_with_positions) + if graph_with_positions: + return graph_with_positions + except Exception as e: + self._module.fail_json(msg="Failed to build graph from DOT data: %s" % e) + + def get_map_config(self): + if not self.dot_data: + self._module.fail_json(msg="'data' is mandatory with state 'present'") + graph = self._build_graph() + nodes = self._get_graph_nodes(graph) + edges = self._get_graph_edges(graph) + icon_ids = self._get_icon_ids() + map_config = { + 'name': self.map_name, + 'label_type': self._get_label_type_id(self.label_type), + 'expandproblem': int(self.expand_problem), + 'highlight': int(self.highlight), + 'width': self.width, + 'height': self.height, + 'selements': self._get_selements(graph, nodes, icon_ids), + 'links': self._get_links(nodes, edges), + } + return map_config + + def _get_label_type_id(self, label_type): + label_type_ids = { + 'label': 0, + 'ip': 1, + 'name': 2, + 'status': 3, + 'nothing': 4, + 'custom': 5, + } + try: + label_type_id = label_type_ids[label_type] + except Exception as e: + self._module.fail_json(msg="Failed to find id for label type '%s': %s" % (label_type, e)) + return label_type_id + + def _get_images_info(self, data, icon_ids): + images = [ + { + 'dot_tag': 'zbx_image', + 'zbx_property': 'iconid_off', + 'mandatory': True + }, + { + 'dot_tag': 'zbx_image_disabled', + 'zbx_property': 'iconid_disabled', + 'mandatory': False + }, + { + 'dot_tag': 'zbx_image_maintenance', + 'zbx_property': 'iconid_maintenance', + 'mandatory': False + }, + { + 'dot_tag': 'zbx_image_problem', + 'zbx_property': 'iconid_on', + 'mandatory': False + } + ] + images_info = {} + default_image = self.default_image if self.default_image else sorted(icon_ids.items())[0][0] + for image in images: + image_name = data.get(image['dot_tag'], None) + if not image_name: + if image['mandatory']: + image_name = default_image + else: + continue + image_name = remove_quotes(image_name) + if image_name in icon_ids: + images_info[image['zbx_property']] = icon_ids[image_name] + if not image['mandatory']: + images_info['use_iconmap'] = 0 + else: + self._module.fail_json(msg="Failed to find id for image '%s'" % image_name) + return images_info + + def _get_element_type(self, data): + types = { + 'host': 0, + 'sysmap': 1, + 'trigger': 2, + 'group': 3, + 'image': 4 + } + element_type = { + 'elementtype': types['image'], + } + if StrictVersion(self.api_version) < StrictVersion('3.4'): + element_type.update({ + 'elementid': "0", + }) + for type_name, type_id in sorted(types.items()): + field_name = 'zbx_' + type_name + if field_name in data: + method_name = '_get_' + type_name + '_id' + element_name = remove_quotes(data[field_name]) + get_element_id = getattr(self, method_name, None) + if get_element_id: + elementid = get_element_id(element_name) + if elementid and int(elementid) > 0: + element_type.update({ + 'elementtype': type_id, + 'label': element_name + }) + if StrictVersion(self.api_version) < StrictVersion('3.4'): + element_type.update({ + 'elementid': elementid, + }) + else: + element_type.update({ + 'elements': [{ + type_name + 'id': elementid, + }], + }) + break + else: + self._module.fail_json(msg="Failed to find id for %s '%s'" % (type_name, element_name)) + return element_type + + # get list of map elements (nodes) + def _get_selements(self, graph, nodes, icon_ids): + selements = [] + icon_sizes = {} + scales = self._get_scales(graph) + for selementid, (node, data) in enumerate(nodes.items(), start=1): + selement = { + 'selementid': selementid + } + data['selementid'] = selementid + + images_info = self._get_images_info(data, icon_ids) + selement.update(images_info) + image_id = images_info['iconid_off'] + if image_id not in icon_sizes: + icon_sizes[image_id] = self._get_icon_size(image_id) + + pos = self._convert_coordinates(data['pos'], scales, icon_sizes[image_id]) + selement.update(pos) + + selement['label'] = remove_quotes(node) + element_type = self._get_element_type(data) + selement.update(element_type) + + label = self._get_label(data) + if label: + selement['label'] = label + + urls = self._get_urls(data) + if urls: + selement['urls'] = urls + + selements.append(selement) + return selements + + def _get_links(self, nodes, edges): + links = {} + for edge in edges: + link_id = tuple(sorted(edge.obj_dict['points'])) + node1, node2 = link_id + data = edge.obj_dict['attributes'] + + if "style" in data and data['style'] == "invis": + continue + + if link_id not in links: + links[link_id] = { + 'selementid1': min(nodes[node1]['selementid'], nodes[node2]['selementid']), + 'selementid2': max(nodes[node1]['selementid'], nodes[node2]['selementid']), + } + link = links[link_id] + + if "color" not in link: + link['color'] = self._get_color_hex(remove_quotes(data.get('color', 'green'))) + + if "zbx_draw_style" not in link: + link['drawtype'] = self._get_link_draw_style_id(remove_quotes(data.get('zbx_draw_style', 'line'))) + + label = self._get_label(data) + if label and "label" not in link: + link['label'] = label + + triggers = self._get_triggers(data) + if triggers: + if "linktriggers" not in link: + link['linktriggers'] = [] + link['linktriggers'] += triggers + + return list(links.values()) + + def _get_urls(self, data): + urls = [] + for url_raw in [remove_quotes(value) for key, value in data.items() if key.startswith("zbx_url")]: + try: + name, url = url_raw.split(':', 1) + except Exception as e: + self._module.fail_json(msg="Failed to parse zbx_url='%s': %s" % (url_raw, e)) + urls.append({ + 'name': name, + 'url': url, + }) + return urls + + def _get_triggers(self, data): + triggers = [] + for trigger_definition in [remove_quotes(value) for key, value in data.items() if key.startswith("zbx_trigger")]: + triggerid = self._get_trigger_id(trigger_definition) + if triggerid: + triggers.append({ + 'triggerid': triggerid, + 'color': self._get_color_hex(remove_quotes(data.get('zbx_trigger_color', 'red'))), + 'drawtype': self._get_link_draw_style_id(remove_quotes(data.get('zbx_trigger_draw_style', 'bold'))), + }) + else: + self._module.fail_json(msg="Failed to find trigger '%s'" % (trigger_definition)) + return triggers + + @staticmethod + def _get_label(data, default=None): + if "zbx_label" in data: + label = remove_quotes(data['zbx_label']).replace('\\n', '\n') + elif "label" in data: + label = remove_quotes(data['label']) + else: + label = default + return label + + def _get_sysmap_id(self, map_name): + exist_map = self._zapi.map.get({'filter': {'name': map_name}}) + if exist_map: + return exist_map[0]['sysmapid'] + return None + + def _get_group_id(self, group_name): + exist_group = self._zapi.hostgroup.get({'filter': {'name': group_name}}) + if exist_group: + return exist_group[0]['groupid'] + return None + + def map_exists(self): + return bool(self.map_id) + + def create_map(self, map_config): + try: + if self._module.check_mode: + self._module.exit_json(changed=True) + result = self._zapi.map.create(map_config) + if result: + return result + except Exception as e: + self._module.fail_json(msg="Failed to create map: %s" % e) + + def update_map(self, map_config): + if not self.map_id: + self._module.fail_json(msg="Failed to update map: map_id is unknown. Try to create_map instead.") + try: + if self._module.check_mode: + self._module.exit_json(changed=True) + map_config['sysmapid'] = self.map_id + result = self._zapi.map.update(map_config) + if result: + return result + except Exception as e: + self._module.fail_json(msg="Failed to update map: %s" % e) + + def delete_map(self): + if not self.map_id: + self._module.fail_json(msg="Failed to delete map: map_id is unknown.") + try: + if self._module.check_mode: + self._module.exit_json(changed=True) + self._zapi.map.delete([self.map_id]) + except Exception as e: + self._module.fail_json(msg="Failed to delete map, Exception: %s" % e) + + def is_exist_map_correct(self, generated_map_config): + exist_map_configs = self._zapi.map.get({ + 'sysmapids': self.map_id, + 'selectLinks': 'extend', + 'selectSelements': 'extend' + }) + exist_map_config = exist_map_configs[0] + if not self._is_dicts_equal(generated_map_config, exist_map_config): + return False + if not self._is_selements_equal(generated_map_config['selements'], exist_map_config['selements']): + return False + self._update_ids(generated_map_config, exist_map_config) + if not self._is_links_equal(generated_map_config['links'], exist_map_config['links']): + return False + return True + + def _get_selements_sort_keys(self): + keys_to_sort = ['label'] + if StrictVersion(self.api_version) < StrictVersion('3.4'): + keys_to_sort.insert(0, 'elementid') + return keys_to_sort + + def _is_selements_equal(self, generated_selements, exist_selements): + if len(generated_selements) != len(exist_selements): + return False + generated_selements_sorted = sorted(generated_selements, key=itemgetter(*self.selements_sort_keys)) + exist_selements_sorted = sorted(exist_selements, key=itemgetter(*self.selements_sort_keys)) + for (generated_selement, exist_selement) in zip(generated_selements_sorted, exist_selements_sorted): + if StrictVersion(self.api_version) >= StrictVersion("3.4"): + if not self._is_elements_equal(generated_selement.get('elements', []), exist_selement.get('elements', [])): + return False + if not self._is_dicts_equal(generated_selement, exist_selement, ['selementid']): + return False + if not self._is_urls_equal(generated_selement.get('urls', []), exist_selement.get('urls', [])): + return False + return True + + def _is_urls_equal(self, generated_urls, exist_urls): + if len(generated_urls) != len(exist_urls): + return False + generated_urls_sorted = sorted(generated_urls, key=itemgetter('name', 'url')) + exist_urls_sorted = sorted(exist_urls, key=itemgetter('name', 'url')) + for (generated_url, exist_url) in zip(generated_urls_sorted, exist_urls_sorted): + if not self._is_dicts_equal(generated_url, exist_url, ['selementid']): + return False + return True + + def _is_elements_equal(self, generated_elements, exist_elements): + if len(generated_elements) != len(exist_elements): + return False + generated_elements_sorted = sorted(generated_elements, key=lambda k: k.values()[0]) + exist_elements_sorted = sorted(exist_elements, key=lambda k: k.values()[0]) + for (generated_element, exist_element) in zip(generated_elements_sorted, exist_elements_sorted): + if not self._is_dicts_equal(generated_element, exist_element, ['selementid']): + return False + return True + + # since generated IDs differ from real Zabbix ones, make real IDs match generated ones + def _update_ids(self, generated_map_config, exist_map_config): + generated_selements_sorted = sorted(generated_map_config['selements'], key=itemgetter(*self.selements_sort_keys)) + exist_selements_sorted = sorted(exist_map_config['selements'], key=itemgetter(*self.selements_sort_keys)) + id_mapping = {} + for (generated_selement, exist_selement) in zip(generated_selements_sorted, exist_selements_sorted): + id_mapping[exist_selement['selementid']] = generated_selement['selementid'] + for link in exist_map_config['links']: + link['selementid1'] = id_mapping[link['selementid1']] + link['selementid2'] = id_mapping[link['selementid2']] + if link['selementid2'] < link['selementid1']: + link['selementid1'], link['selementid2'] = link['selementid2'], link['selementid1'] + + def _is_links_equal(self, generated_links, exist_links): + if len(generated_links) != len(exist_links): + return False + generated_links_sorted = sorted(generated_links, key=itemgetter('selementid1', 'selementid2', 'color', 'drawtype')) + exist_links_sorted = sorted(exist_links, key=itemgetter('selementid1', 'selementid2', 'color', 'drawtype')) + for (generated_link, exist_link) in zip(generated_links_sorted, exist_links_sorted): + if not self._is_dicts_equal(generated_link, exist_link, ['selementid1', 'selementid2']): + return False + if not self._is_triggers_equal(generated_link.get('linktriggers', []), exist_link.get('linktriggers', [])): + return False + return True + + def _is_triggers_equal(self, generated_triggers, exist_triggers): + if len(generated_triggers) != len(exist_triggers): + return False + generated_triggers_sorted = sorted(generated_triggers, key=itemgetter('triggerid')) + exist_triggers_sorted = sorted(exist_triggers, key=itemgetter('triggerid')) + for (generated_trigger, exist_trigger) in zip(generated_triggers_sorted, exist_triggers_sorted): + if not self._is_dicts_equal(generated_trigger, exist_trigger): + return False + return True + + @staticmethod + def _is_dicts_equal(d1, d2, exclude_keys=None): + if exclude_keys is None: + exclude_keys = [] + for key in d1.keys(): + if isinstance(d1[key], dict) or isinstance(d1[key], list): + continue + if key in exclude_keys: + continue + # compare as strings since Zabbix API returns everything as strings + if key not in d2 or str(d2[key]) != str(d1[key]): + return False + return True + + def _get_host_id(self, hostname): + hostid = self._zapi.host.get({'filter': {'host': hostname}}) + if hostid: + return str(hostid[0]['hostid']) + + def _get_trigger_id(self, trigger_definition): + try: + host, trigger = trigger_definition.split(':', 1) + except Exception as e: + self._module.fail_json(msg="Failed to parse zbx_trigger='%s': %s" % (trigger_definition, e)) + triggerid = self._zapi.trigger.get({ + 'host': host, + 'filter': { + 'description': trigger + } + }) + if triggerid: + return str(triggerid[0]['triggerid']) + + def _get_icon_ids(self): + icons_list = self._zapi.image.get({}) + icon_ids = {} + for icon in icons_list: + icon_ids[icon['name']] = icon['imageid'] + return icon_ids + + def _get_icon_size(self, icon_id): + icons_list = self._zapi.image.get({ + 'imageids': [ + icon_id + ], + 'select_image': True + }) + if len(icons_list) > 0: + icon_base64 = icons_list[0]['image'] + else: + self._module.fail_json(msg="Failed to find image with id %s" % icon_id) + image = Image.open(BytesIO(base64.b64decode(icon_base64))) + icon_width, icon_height = image.size + return icon_width, icon_height + + @staticmethod + def _get_node_attributes(node): + attr = {} + if "attributes" in node.obj_dict: + attr.update(node.obj_dict['attributes']) + pos = node.get_pos() + if pos is not None: + pos = remove_quotes(pos) + xx, yy = pos.split(",") + attr['pos'] = (float(xx), float(yy)) + return attr + + def _get_graph_nodes(self, parent): + nodes = {} + for node in parent.get_nodes(): + node_name = node.get_name() + if node_name in ('node', 'graph', 'edge'): + continue + nodes[node_name] = self._get_node_attributes(node) + for subgraph in parent.get_subgraphs(): + nodes.update(self._get_graph_nodes(subgraph)) + return nodes + + def _get_graph_edges(self, parent): + edges = [] + for edge in parent.get_edges(): + edges.append(edge) + for subgraph in parent.get_subgraphs(): + edges += self._get_graph_edges(subgraph) + return edges + + def _get_scales(self, graph): + bb = remove_quotes(graph.get_bb()) + min_x, min_y, max_x, max_y = bb.split(",") + scale_x = (self.width - self.margin * 2) / (float(max_x) - float(min_x)) if float(max_x) != float(min_x) else 0 + scale_y = (self.height - self.margin * 2) / (float(max_y) - float(min_y)) if float(max_y) != float(min_y) else 0 + return { + 'min_x': float(min_x), + 'min_y': float(min_y), + 'max_x': float(max_x), + 'max_y': float(max_y), + 'scale_x': float(scale_x), + 'scale_y': float(scale_y), + } + + # transform Graphviz coordinates to Zabbix's ones + def _convert_coordinates(self, pos, scales, icon_size): + return { + 'x': int((pos[0] - scales['min_x']) * scales['scale_x'] - icon_size[0] / 2 + self.margin), + 'y': int((scales['max_y'] - pos[1] + scales['min_y']) * scales['scale_y'] - icon_size[1] / 2 + self.margin), + } + + def _get_color_hex(self, color_name): + if color_name.startswith('#'): + color_hex = color_name + else: + try: + color_hex = webcolors.name_to_hex(color_name) + except Exception as e: + self._module.fail_json(msg="Failed to get RGB hex for color '%s': %s" % (color_name, e)) + color_hex = color_hex.strip('#').upper() + return color_hex + + def _get_link_draw_style_id(self, draw_style): + draw_style_ids = { + 'line': 0, + 'bold': 2, + 'dotted': 3, + 'dashed': 4 + } + try: + draw_style_id = draw_style_ids[draw_style] + except Exception as e: + self._module.fail_json(msg="Failed to find id for draw type '%s': %s" % (draw_style, e)) + return draw_style_id + + +# If a string has single or double quotes around it, remove them. +def remove_quotes(s): + if (s[0] == s[-1]) and s.startswith(("'", '"')): + s = s[1:-1] + return s + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(type='str', required=True, aliases=['url']), + 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), + timeout=dict(type='int', default=10), + validate_certs=dict(type='bool', required=False, default=True), + name=dict(type='str', required=True, aliases=['map_name']), + data=dict(type='str', required=False, aliases=['dot_data']), + width=dict(type='int', default=800), + height=dict(type='int', default=600), + state=dict(default="present", choices=['present', 'absent']), + default_image=dict(type='str', required=False, aliases=['image']), + margin=dict(type='int', default=40), + expand_problem=dict(type='bool', default=True), + highlight=dict(type='bool', default=True), + label_type=dict(type='str', default='name', choices=['label', 'ip', 'name', 'status', 'nothing', 'custom']), + ), + supports_check_mode=True + ) + + if not HAS_ZABBIX_API: + module.fail_json(msg="Missing required zabbix-api module (check docs or install with: pip install zabbix-api)") + if not HAS_PYDOTPLUS: + module.fail_json(msg="Missing required pydotplus module (check docs or install with: pip install pydotplus)") + if not HAS_WEBCOLORS: + module.fail_json(msg="Missing required webcolors module (check docs or install with: pip install webcolors)") + if not HAS_PIL: + module.fail_json(msg="Missing required Pillow module (check docs or install with: pip install Pillow)") + + server_url = module.params['server_url'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] + timeout = module.params['timeout'] + validate_certs = module.params['validate_certs'] + + 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.login(login_user, login_password) + except Exception as e: + module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) + + sysmap = Map(module, zbx) + + if sysmap.state == "absent": + if sysmap.map_exists(): + sysmap.delete_map() + module.exit_json(changed=True, result="Successfully deleted map: %s" % sysmap.map_name) + else: + module.exit_json(changed=False) + else: + map_config = sysmap.get_map_config() + if sysmap.map_exists(): + if sysmap.is_exist_map_correct(map_config): + module.exit_json(changed=False) + else: + sysmap.update_map(map_config) + module.exit_json(changed=True, result="Successfully updated map: %s" % sysmap.map_name) + else: + sysmap.create_map(map_config) + module.exit_json(changed=True, result="Successfully created map: %s" % sysmap.map_name) + + +if __name__ == '__main__': + main()