diff --git a/lib/ansible/modules/monitoring/zabbix/zabbix_action.py b/lib/ansible/modules/monitoring/zabbix/zabbix_action.py new file mode 100644 index 0000000000..2933181bcf --- /dev/null +++ b/lib/ansible/modules/monitoring/zabbix/zabbix_action.py @@ -0,0 +1,1673 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: zabbix_action + +short_description: Create/Delete/Update Zabbix actions + +version_added: "2.8" + +description: + - This module allows you to create, modify and delete Zabbix actions. + +author: + - Ruben Tsirunyan (@rubentsirunyan) + - Ruben Harutyunov (@K-DOT) + +requirements: + - zabbix-api + +options: + name: + description: + - Name of the action + required: true + event_source: + description: + - Type of events that the action will handle. + required: true + choices: ['trigger', 'discovery', 'auto_registration', 'internal'] + state: + description: + - State of the action. + - On C(present), it will create an action if it does not exist or update the action if the associated data is different. + - On C(absent), it will remove the action if it exists. + choices: ['present', 'absent'] + default: 'present' + status: + description: + - Monitoring status of the action. + choices: ['enabled', 'disabled'] + default: 'enabled' + esc_period: + description: + - Default operation step duration. Must be greater than 60 seconds. Accepts seconds, time unit with suffix and user macro + default: '60' + conditions: + type: list + description: + - List of dictionaries of conditions to evaluate. + - For more information about suboptions of this options please + check out Zabbix API documentation U(https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition) + suboptions: + type: + description: Type (label) of the condition + choices: + # trigger + - host_group + - host + - trigger + - trigger_name + - trigger_severity + - time_period + - host_template + - application + - maintenance_status + - event_tag + - event_tag_value + # discovery + - host_IP + - discovered_service_type + - discovered_service_port + - discovery_status + - uptime_or_downtime_duration + - received_value + - discovery_rule + - discovery_check + - proxy + - discovery_object + # auto_registration + - proxy + - host_name + - host_metadata + # internal + - host_group + - host + - host_template + - application + - event_type + value: + description: + - Value to compare with. + - When I(type) is set to C(discovery_status), the choices + are C(up), C(down), C(discovered), C(lost) + - When I(type) is set to C(discovery_object), the choices + are C(host), C(service) + - When I(type) is set to C(event_type), the choices + are C(item in not supported state), C(item in normal state), + C(LLD rule in not supported state), + C(LLD rule in normal state), C(trigger in unknown state), C(trigger in normal state) + - Besides the above options, this is usualy either the name + of the object or a string to compare with. + operator: + description: + - Condition operator. + choices: + - '=' + - '<>' + - 'like' + - 'not like' + - 'in' + - '>=' + - '<=' + - 'not in' + formulaid: + description: + - Arbitrary unique ID that is used to reference the condition from a custom expression. + - Can only contain capital-case letters. + formula: + description: + - User-defined expression to be used for evaluating conditions of filters with a custom expression. + - The expression must contain IDs that reference specific filter conditions by its formulaid. + - The IDs used in the expression must exactly match the ones + defined in the filter conditions. No condition can remain unused or omitted. + - Required for custom expression filters. + default_message: + description: + - Problem message default text. + default_subject: + description: + - Problem message default subject. + recovery_default_message: + description: + - Recovery message text. + - Works only with >= Zabbix 3.2 + recovery_default_subject: + description: + - Recovery message subject. + - Works only with >= Zabbix 3.2 + acknowledge_default_message: + description: + - Acknowledge operation message text. + - Works only with >= Zabbix 3.4 + acknowledge_default_subject: + description: + - Acknowledge operation message subject. + - Works only with >= Zabbix 3.4 + operations: + type: list + description: + - List of action operations + suboptions: + type: + description: + - Type of operation. + choices: + - send_message + - remote_command + - add_host + - remove_host + - add_to_host_group + - remove_from_host_group + - link_to_template + - unlink_from_template + - enable_host + - disable_host + - set_host_inventory_mode + esc_period: + description: + - Duration of an escalation step in seconds. + - Must be greater than 60 seconds. + - Accepts seconds, time unit with suffix and user macro. + - If set to 0 or 0s, the default action escalation period will be used. + default: 0s + esc_step_from: + description: + - Step to start escalation from. + default: 1 + esc_step_to: + description: + - Step to end escalation at. + default: 1 + send_to_groups: + type: list + description: + - User groups to send messages to. + send_to_users: + type: list + description: + - Users to send messages to. + message: + description: + - Operation message text. + subject: + description: + - Operation message subject. + media_type: + description: + - Media type that will be used to send the message. + command_type: + description: + - Type of operation command. + - Required when I(type=remote_command). + choices: + - custom_script + - ipmi + - ssh + - telnet + - global_script + command: + description: + - Command to run. + - Required when I(type=remote_command) and I(command_type!=global_script). + execute_on: + description: + - Target on which the custom script operation command will be executed. + - Required when I(type=remote_command) and I(command_type=custom_script) + choices: + - agent + - server + - proxy + run_on_groups: + description: + - Host groups to run remote commands on + - Required when I(type=remote_command) if I(run_on_hosts) is not set + run_on_hosts: + description: + - Hosts to run remote commands on + - Required when I(type=remote_command) if I(run_on_groups) is not set + ssh_auth_type: + description: + - Authentication method used for SSH commands. + - Required when I(type=remote_command) and I(command_type=ssh) + choices: + - password + - public_key + ssh_privatekey_file: + description: + - Name of the private key file used for SSH commands with public key authentication. + - Required when I(type=remote_command) and I(command_type=ssh) + ssh_publickey_file: + description: + - Name of the public key file used for SSH commands with public key authentication. + - Required when I(type=remote_command) and I(command_type=ssh) + username: + description: + - User name used for authentication. + - Required when I(type=remote_command) and I(command_type in [ssh, telnet]) + password: + description: + - Password used for authentication. + - Required when I(type=remote_command) and I(command_type in [ssh, telnet]) + port: + description: + - Port number used for authentication. + - Required when I(type=remote_command) and I(command_type in [ssh, telnet]) + script_name: + description: + - The name of script used for global script commands. + - Required when I(type=remote_command) and I(command_type=global_script) + recovery_operations: + type: list + description: + - List of recovery operations + - C(Suboptions) are the same as I(operations) + - Works only with >= Zabbix 3.2 + acknowledge_operations: + type: list + description: + - List of acknowledge operations + - C(Suboptions) are the same as I(operations) + - Works only with >= Zabbix 3.4 + +notes: + - Only Zabbix Server >= 3.0 is supported. + + +extends_documentation_fragment: + - zabbix +''' + +EXAMPLES = ''' +# Trigger action with only one condition +- name: Deploy trigger action + zabbix_action: + server_url: "http://zabbix.example.com/zabbix/" + login_user: Admin + login_password: secret + name: "Send alerts to Admin" + event_source: 'trigger' + state: present + status: enabled + conditions: + - type: 'trigger_severity' + operator: '>=' + value: 'Information' + operations: + - type: send_message + subject: "Something bad is happening" + message: "Come on, guys do something" + media_type: 'Email' + send_to_users: + - 'Admin' + +# Trigger action with multiple conditions and operations +- name: Deploy trigger action + zabbix_action: + server_url: "http://zabbix.example.com/zabbix/" + login_user: Admin + login_password: secret + name: "Send alerts to Admin" + event_source: 'trigger' + state: present + status: enabled + conditions: + - type: 'trigger_name' + operator: 'like' + value: 'Zabbix agent is unreachable' + formulaid: A + - type: 'trigger_severity' + operator: '>=' + value: 'disaster' + formulaid: B + formula: A or B + operations: + - type: send_message + media_type: 'Email' + send_to_users: + - 'Admin' + - type: remote_command + command: 'systemctl restart zabbix-agent' + run_on_hosts: + - 0 + +# Trigger action with recovery and aknowledge operations +- name: Deploy trigger action + zabbix_action: + server_url: "http://zabbix.example.com/zabbix/" + login_user: Admin + login_password: secret + name: "Send alerts to Admin" + event_source: 'trigger' + state: present + status: enabled + conditions: + - type: 'trigger_severity' + operator: '>=' + value: 'Information' + operations: + - type: send_message + subject: "Something bad is happening" + message: "Come on, guys do something" + media_type: 'Email' + send_to_users: + - 'Admin' + recovery_operations: + - type: send_message + subject: "Host is down" + message: "Come on, guys do something" + media_type: 'Email' + send_to_users: + - 'Admin' + acknowledge_operations: + - type: send_message + media_type: 'Email' + send_to_users: + - 'Admin' +''' + +RETURN = ''' +msg: + description: The result of the operation + returned: success + type: string + sample: 'Action Deleted: Register webservers, ID: 0001' +''' + +try: + from zabbix_api import ZabbixAPI + HAS_ZABBIX_API = True +except ImportError: + HAS_ZABBIX_API = False + +from ansible.module_utils.basic import AnsibleModule + + +class Zapi(object): + """ + A simple wrapper over the Zabbix API + """ + def __init__(self, module, zbx): + self._module = module + self._zapi = zbx + + def check_if_action_exists(self, name): + """Check if action exists. + + Args: + name: Name of the action. + + Returns: + The return value. True for success, False otherwise. + + """ + try: + _action = self._zapi.action.get({ + "selectOperations": "extend", + "selectRecoveryOperations": "extend", + "selectAcknowledgeOperations": "extend", + "selectFilter": "extend", + 'selectInventory': 'extend', + 'filter': {'name': [name]} + }) + if len(_action) > 0: + _action[0]['recovery_operations'] = _action[0].pop('recoveryOperations', []) + _action[0]['acknowledge_operations'] = _action[0].pop('acknowledgeOperations', []) + return _action + except Exception as e: + self._module.fail_json(msg="Failed to check if action '%s' exists: %s" % (name, e)) + + def get_action_by_name(self, name): + """Get action by name + + Args: + name: Name of the action. + + Returns: + dict: Zabbix action + + """ + try: + action_list = self._zapi.action.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [name]} + }) + if len(action_list) < 1: + self._module.fail_json(msg="Action not found: " % name) + else: + return action_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get ID of '%s': %s" % (name, e)) + + def get_host_by_host_name(self, host_name): + """Get host by host name + + Args: + host_name: host name. + + Returns: + host matching host name + + """ + try: + host_list = self._zapi.host.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'host': [host_name]} + }) + if len(host_list) < 1: + self._module.fail_json(msg="Host not found: %s" % host_name) + else: + return host_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get host '%s': %s" % (host_name, e)) + + def get_hostgroup_by_hostgroup_name(self, hostgroup_name): + """Get host group by host group name + + Args: + hostgroup_name: host group name. + + Returns: + host group matching host group name + + """ + try: + hostgroup_list = self._zapi.hostgroup.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [hostgroup_name]} + }) + if len(hostgroup_list) < 1: + self._module.fail_json(msg="Host group not found: %s" % hostgroup_name) + else: + return hostgroup_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get host group '%s': %s" % (hostgroup_name, e)) + + def get_template_by_template_name(self, template_name): + """Get template by template name + + Args: + template_name: template name. + + Returns: + template matching template name + + """ + try: + template_list = self._zapi.template.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'host': [template_name]} + }) + if len(template_list) < 1: + self._module.fail_json(msg="Template not found: %s" % template_name) + else: + return template_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get template '%s': %s" % (template_name, e)) + + def get_trigger_by_trigger_name(self, trigger_name): + """Get trigger by trigger name + + Args: + trigger_name: trigger name. + + Returns: + trigger matching trigger name + + """ + try: + trigger_list = self._zapi.trigger.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'description': [trigger_name]} + }) + if len(trigger_list) < 1: + self._module.fail_json(msg="Trigger not found: %s" % trigger_name) + else: + return trigger_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get trigger '%s': %s" % (trigger_name, e)) + + def get_discovery_rule_by_discovery_rule_name(self, discovery_rule_name): + """Get discovery rule by discovery rule name + + Args: + discovery_rule_name: discovery rule name. + + Returns: + discovery rule matching discovery rule name + + """ + try: + discovery_rule_list = self._zapi.drule.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [discovery_rule_name]} + }) + if len(discovery_rule_list) < 1: + self._module.fail_json(msg="Discovery rule not found: %s" % discovery_rule_name) + else: + return discovery_rule_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get discovery rule '%s': %s" % (discovery_rule_name, e)) + + def get_discovery_check_by_discovery_check_name(self, discovery_check_name): + """Get discovery check by discovery check name + + Args: + discovery_check_name: discovery check name. + + Returns: + discovery check matching discovery check name + + """ + try: + discovery_check_list = self._zapi.dcheck.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [discovery_check_name]} + }) + if len(discovery_check_list) < 1: + self._module.fail_json(msg="Discovery check not found: %s" % discovery_check_name) + else: + return discovery_check_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get discovery check '%s': %s" % (discovery_check_name, e)) + + def get_proxy_by_proxy_name(self, proxy_name): + """Get proxy by proxy name + + Args: + proxy_name: proxy name. + + Returns: + proxy matching proxy name + + """ + try: + proxy_list = self._zapi.proxy.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'host': [proxy_name]} + }) + if len(proxy_list) < 1: + self._module.fail_json(msg="Proxy not found: %s" % proxy_name) + else: + return proxy_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get proxy '%s': %s" % (proxy_name, e)) + + def get_mediatype_by_mediatype_name(self, mediatype_name): + """Get mediatype by mediatype name + + Args: + mediatype_name: mediatype name + + Returns: + mediatype matching mediatype name + + """ + try: + mediatype_list = self._zapi.mediatype.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'description': [mediatype_name]} + }) + if len(mediatype_list) < 1: + self._module.fail_json(msg="Media type not found: %s" % mediatype_name) + else: + return mediatype_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get mediatype '%s': %s" % (mediatype_name, e)) + + def get_user_by_user_name(self, user_name): + """Get user by user name + + Args: + user_name: user name + + Returns: + user matching user name + + """ + try: + user_list = self._zapi.user.get({ + 'output': 'extend', + 'selectInventory': + 'extend', 'filter': {'alias': [user_name]} + }) + if len(user_list) < 1: + self._module.fail_json(msg="User not found: %s" % user_name) + else: + return user_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get user '%s': %s" % (user_name, e)) + + def get_usergroup_by_usergroup_name(self, usergroup_name): + """Get usergroup by usergroup name + + Args: + usergroup_name: usergroup name + + Returns: + usergroup matching usergroup name + + """ + try: + usergroup_list = self._zapi.usergroup.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [usergroup_name]} + }) + if len(usergroup_list) < 1: + self._module.fail_json(msg="User group not found: %s" % usergroup_name) + else: + return usergroup_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get user group '%s': %s" % (usergroup_name, e)) + + # get script by script name + def get_script_by_script_name(self, script_name): + """Get script by script name + + Args: + script_name: script name + + Returns: + script matching script name + + """ + try: + if script_name is None: + return {} + script_list = self._zapi.script.get({ + 'output': 'extend', + 'selectInventory': 'extend', + 'filter': {'name': [script_name]} + }) + if len(script_list) < 1: + self._module.fail_json(msg="Script not found: %s" % script_name) + else: + return script_list[0] + except Exception as e: + self._module.fail_json(msg="Failed to get script '%s': %s" % (script_name, e)) + + +class Action(object): + """ + Restructures the user defined action data to fit the Zabbix API requirements + """ + def __init__(self, module, zbx, zapi_wrapper): + self._module = module + self._zapi = zbx + self._zapi_wrapper = zapi_wrapper + + def _construct_parameters(self, **kwargs): + """Contruct parameters. + + Args: + **kwargs: Arbitrary keyword parameters. + + Returns: + dict: dictionary of specified parameters + """ + return { + 'name': kwargs['name'], + 'eventsource': to_numeric_value([ + 'trigger', + 'discovery', + 'auto_registration', + 'internal'], kwargs['event_source']), + 'esc_period': kwargs.get('esc_period'), + 'filter': kwargs['conditions'], + 'def_longdata': kwargs['default_message'], + 'def_shortdata': kwargs['default_subject'], + 'r_longdata': kwargs['recovery_default_message'], + 'r_shortdata': kwargs['recovery_default_subject'], + 'ack_longdata': kwargs['acknowledge_default_message'], + 'ack_shortdata': kwargs['acknowledge_default_subject'], + 'operations': kwargs['operations'], + 'recovery_operations': kwargs.get('recovery_operations'), + 'acknowledge_operations': kwargs.get('acknowledge_operations'), + 'status': to_numeric_value([ + 'enabled', + 'disabled'], kwargs['status']) + } + + def check_difference(self, **kwargs): + """Check difference between action and user specified parameters. + + Args: + **kwargs: Arbitrary keyword parameters. + + Returns: + dict: dictionary of differences + """ + existing_action = convert_unicode_to_str(self._zapi_wrapper.check_if_action_exists(kwargs['name'])[0]) + parameters = convert_unicode_to_str(self._construct_parameters(**kwargs)) + change_parameters = {} + _diff = cleanup_data(compare_dictionaries(parameters, existing_action, change_parameters)) + if ('recovery_operations' in cleanup_data(existing_action) and + 'acknowledge_operations' not in cleanup_data(parameters)): + _diff['recovery_operations'] = [] + if ('acknowledge_operations' in cleanup_data(existing_action) and + 'acknowledge_operations' not in cleanup_data(parameters)): + _diff['acknowledge_operations'] = [] + return _diff + + def update_action(self, **kwargs): + """Update action. + + Args: + **kwargs: Arbitrary keyword parameters. + + Returns: + action: updated action + """ + try: + if self._module.check_mode: + self._module.exit_json(msg="Action would be updated if check mode was not specified: %s" % kwargs, changed=True) + kwargs['actionid'] = kwargs.pop('action_id') + return self._zapi.action.update(kwargs) + except Exception as e: + self._module.fail_json(msg="Failed to update action '%s': %s" % (kwargs['actionid'], e)) + + def add_action(self, **kwargs): + """Add action. + + Args: + **kwargs: Arbitrary keyword parameters. + + Returns: + action: added action + """ + try: + if self._module.check_mode: + self._module.exit_json(msg="Action would be added if check mode was not specified", changed=True) + parameters = self._construct_parameters(**kwargs) + action_list = self._zapi.action.create(parameters) + return action_list['actionids'][0] + except Exception as e: + self._module.fail_json(msg="Failed to create action '%s': %s" % (kwargs['name'], e)) + + def delete_action(self, action_id): + """Delete action. + + Args: + action_id: Action id + + Returns: + action: deleted action + """ + try: + if self._module.check_mode: + self._module.exit_json(msg="Action would be deleted if check mode was not specified", changed=True) + return self._zapi.action.delete([action_id]) + except Exception as e: + self._module.fail_json(msg="Failed to delete action '%s': %s" % (action_id, e)) + + +class Operations(object): + """ + Restructures the user defined operation data to fit the Zabbix API requirements + """ + def __init__(self, module, zbx, zapi_wrapper): + self._module = module + # self._zapi = zbx + self._zapi_wrapper = zapi_wrapper + + def _construct_operationtype(self, operation): + """Construct operation type. + + Args: + operation: operation to construct + + Returns: + str: constructed operation + """ + try: + return to_numeric_value([ + "send_message", + "remote_command", + "add_host", + "remove_host", + "add_to_host_group", + "remove_from_host_group", + "link_to_template", + "unlink_from_template", + "enable_host", + "disable_host", + "set_host_inventory_mode"], operation['type'] + ) + except Exception as e: + self._module.fail_json(msg="Unsupported value '%s' for operation type." % operation['type']) + + def _construct_opmessage(self, operation): + """Construct operation message. + + Args: + operation: operation to construct the message + + Returns: + dict: constructed operation message + """ + try: + return { + 'default_msg': '0' if 'message' in operation or 'subject' in operation else '1', + 'mediatypeid': self._zapi_wrapper.get_mediatype_by_mediatype_name( + operation.get('media_type') + )['mediatypeid'] if operation.get('media_type') is not None else None, + 'message': operation.get('message'), + 'subject': operation.get('subject'), + } + except Exception as e: + self._module.fail_json(msg="Failed to construct operation message. The error was: %s" % e) + + def _construct_opmessage_usr(self, operation): + """Construct operation message user. + + Args: + operation: operation to construct the message user + + Returns: + list: constructed operation message user or None if oprtation not found + """ + if operation.get('send_to_users') is None: + return None + return [{ + 'userid': self._zapi_wrapper.get_user_by_user_name(_user)['userid'] + } for _user in operation.get('send_to_users')] + + def _construct_opmessage_grp(self, operation): + """Construct operation message group. + + Args: + operation: operation to construct the message group + + Returns: + list: constructed operation message group or None if operation not found + """ + if operation.get('send_to_groups') is None: + return None + return [{ + 'usrgrpid': self._zapi_wrapper.get_usergroup_by_usergroup_name(_group)['usrgrpid'] + } for _group in operation.get('send_to_groups')] + + def _construct_opcommand(self, operation): + """Construct operation command. + + Args: + operation: operation to construct command + + Returns: + list: constructed operation command + """ + try: + return { + 'type': to_numeric_value([ + 'custom_script', + 'ipmi', + 'ssh', + 'telnet', + 'global_script'], operation.get('command_type', 'custom_script')), + 'command': operation.get('command'), + 'execute_on': to_numeric_value([ + 'agent', + 'server', + 'proxy'], operation.get('execute_on', 'server')), + 'scriptid': self._zapi_wrapper.get_script_by_script_name( + operation.get('script_name') + ).get('scriptid'), + 'authtype': to_numeric_value([ + 'password', + 'private_key' + ], operation.get('ssh_auth_type', 'password')), + 'privatekey': operation.get('ssh_privatekey_file'), + 'publickey': operation.get('ssh_publickey_file'), + 'username': operation.get('username'), + 'password': operation.get('password'), + 'port': operation.get('port') + } + except Exception as e: + self._module.fail_json(msg="Failed to construct operation command. The error was: %s" % e) + + def _construct_opcommand_hst(self, operation): + """Construct operation command host. + + Args: + operation: operation to construct command host + + Returns: + list: constructed operation command host + """ + if operation.get('run_on_hosts') is None: + return None + return [{ + 'hostid': self._zapi_wrapper.get_host_by_host_name(_host)['hostid'] + } if str(_host) != '0' else {'hostid': '0'} for _host in operation.get('run_on_hosts')] + + def _construct_opcommand_grp(self, operation): + """Construct operation command group. + + Args: + operation: operation to construct command group + + Returns: + list: constructed operation command group + """ + if operation.get('run_on_groups') is None: + return None + return [{ + 'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['hostid'] + } for _group in operation.get('run_on_groups')] + + def _construct_opgroup(self, operation): + """Construct operation group. + + Args: + operation: operation to construct group + + Returns: + list: constructed operation group + """ + return [{ + 'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['groupid'] + } for _group in operation.get('host_groups', [])] + + def _construct_optemplate(self, operation): + """Construct operation template. + + Args: + operation: operation to construct template + + Returns: + list: constructed operation template + """ + return [{ + 'templateid': self._zapi_wrapper.get_template_by_template_name(_template)['templateid'] + } for _template in operation.get('templates', [])] + + def _construct_opinventory(self, operation): + """Construct operation inventory. + + Args: + operation: operation to construct inventory + + Returns: + dict: constructed operation inventory + """ + return {'inventory_mode': operation.get('inventory')} + + def construct_the_data(self, operations): + """Construct the oprtation data using helper methods. + + Args: + operation: operation to construct + + Returns: + list: constructed operation data + """ + constructed_data = [] + for op in operations: + operation_type = self._construct_operationtype(op) + constructed_operation = { + 'operationtype': operation_type, + 'esc_period': op.get('esc_period'), + 'esc_step_from': op.get('esc_step_from'), + 'esc_step_to': op.get('esc_step_to') + } + # Send Message type + if constructed_operation['operationtype'] == '0': + constructed_operation['opmessage'] = self._construct_opmessage(op) + constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op) + constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op) + + # Send Command type + if constructed_operation['operationtype'] == '1': + constructed_operation['opcommand'] = self._construct_opcommand(op) + constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op) + constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op) + + # Add to/Remove from host group + if constructed_operation['operationtype'] in ('4', '5'): + constructed_operation['opgroup'] = self._construct_opgroup(op) + + # Link/Unlink template + if constructed_operation['operationtype'] in ('6', '7'): + constructed_operation['optemplate'] = self._construct_optemplate(op) + + # Set inventory mode + if constructed_operation['operationtype'] == '10': + constructed_operation['opinventory'] = self._construct_opinventory(op) + + constructed_data.append(constructed_operation) + + return cleanup_data(constructed_data) + + +class RecoveryOperations(Operations): + """ + Restructures the user defined recovery operations data to fit the Zabbix API requirements + """ + def _construct_operationtype(self, operation): + """Construct operation type. + + Args: + operation: operation to construct type + + Returns: + str: constructed operation type + """ + try: + return to_numeric_value([ + "send_message", + "remote_command", + None, + None, + None, + None, + None, + None, + None, + None, + None, + "notify_all_involved"], operation['type'] + ) + except Exception as e: + self._module.fail_json(msg="Unsupported value '%s' for recovery operation type." % operation['type']) + + def construct_the_data(self, operations): + """Construct the recovery operations data using helper methods. + + Args: + operation: operation to construct + + Returns: + list: constructed recovery operations data + """ + if operations is None: + return None + constructed_data = [] + for op in operations: + operation_type = self._construct_operationtype(op) + constructed_operation = { + 'operationtype': operation_type, + } + + # Send Message type + if constructed_operation['operationtype'] in ('0', '11'): + constructed_operation['opmessage'] = self._construct_opmessage(op) + constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op) + constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op) + + # Send Command type + if constructed_operation['operationtype'] == '1': + constructed_operation['opcommand'] = self._construct_opcommand(op) + constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op) + constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op) + + constructed_data.append(constructed_operation) + + return cleanup_data(constructed_data) + + +class AcknowledgeOperations(Operations): + """ + Restructures the user defined acknowledge operations data to fit the Zabbix API requirements + """ + def _construct_operationtype(self, operation): + """Construct operation type. + + Args: + operation: operation to construct type + + Returns: + str: constructed operation type + """ + try: + return to_numeric_value([ + "send_message", + "remote_command", + None, + None, + None, + None, + None, + None, + None, + None, + None, + "notify_all_involved"], operation['type'] + ) + except Exception as e: + self._module.fail_json(msg="Unsupported value '%s' for acknowledge operation type." % operation['type']) + + def construct_the_data(self, operations): + """Construct the acknowledge operations data using helper methods. + + Args: + operation: operation to construct + + Returns: + list: constructed acknowledge operations data + """ + if operations is None: + return None + constructed_data = [] + for op in operations: + operation_type = self._construct_operationtype(op) + constructed_operation = { + 'operationtype': operation_type, + } + + # Send Message type + if constructed_operation['operationtype'] in ('0', '11'): + constructed_operation['opmessage'] = self._construct_opmessage(op) + constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op) + constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op) + + # Send Command type + if constructed_operation['operationtype'] == '1': + constructed_operation['opcommand'] = self._construct_opcommand(op) + constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op) + constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op) + + constructed_data.append(constructed_operation) + + return cleanup_data(constructed_data) + + +class Filter(object): + """ + Restructures the user defined filter conditions to fit the Zabbix API requirements + """ + def __init__(self, module, zbx, zapi_wrapper): + self._module = module + self._zapi = zbx + self._zapi_wrapper = zapi_wrapper + + def _construct_evaltype(self, _eval, _conditions): + """Construct the eval type + + Args: + _eval: zabbix condition evaluation formula + _conditions: list of conditions to check + + Returns: + dict: constructed acknowledge operations data + """ + if len(_conditions) <= 1: + return { + 'evaltype': '0', + 'formula': None + } + return { + 'evaltype': '3', + 'formula': _eval + } + + def _construct_conditiontype(self, _condition): + """Construct the condition type + + Args: + _condition: condition to check + + Returns: + str: constructed condition type data + """ + try: + return to_numeric_value([ + "host_group", + "host", + "trigger", + "trigger_name", + "trigger_severity", + "trigger_value", + "time_period", + "host_ip", + "discovered_service_type", + "discovered_service_port", + "discovery_status", + "uptime_or_downtime_duration", + "received_value", + "host_template", + None, + "application", + "maintenance_status", + None, + "discovery_rule", + "discovery_check", + "proxy", + "discovery_object", + "host_name", + "event_type", + "host_metadata", + "event_tag", + "event_tag_value"], _condition['type'] + ) + except Exception as e: + self._module.fail_json(msg="Unsupported value '%s' for condition type." % _condition['type']) + + def _construct_operator(self, _condition): + """Construct operator + + Args: + _condition: condition to construct + + Returns: + str: constructed operator + """ + try: + return to_numeric_value([ + "=", + "<>", + "like", + "not like", + "in", + ">=", + "<=", + "not in"], _condition['operator'] + ) + except Exception as e: + self._module.fail_json(msg="Unsupported value '%s' for operator." % _condition['operator']) + + def _construct_value(self, conditiontype, value): + """Construct operator + + Args: + conditiontype: type of condition to construct + value: value to construct + + Returns: + str: constructed value + """ + try: + # Host group + if conditiontype == '0': + return self._zapi_wrapper.get_hostgroup_by_hostgroup_name(value)['groupid'] + # Host + if conditiontype == '1': + return self._zapi_wrapper.get_host_by_host_name(value)['hostid'] + # Trigger + if conditiontype == '2': + return self._zapi_wrapper.get_trigger_by_trigger_name(value)['triggerid'] + # Trigger name: return as is + # Trigger severity + if conditiontype == '4': + return to_numeric_value([ + "not classified", + "information", + "warning", + "average", + "high", + "disaster"], value or "not classified" + ) + + # Trigger value + if conditiontype == '5': + return to_numeric_value([ + "ok", + "problem"], value or "ok" + ) + # Time period: return as is + # Host IP: return as is + # Discovered service type + if conditiontype == '8': + return to_numeric_value([ + "SSH", + "LDAP", + "SMTP", + "FTP", + "HTTP", + "POP", + "NNTP", + "IMAP", + "TCP", + "Zabbix agent", + "SNMPv1 agent", + "SNMPv2 agent", + "ICMP ping", + "SNMPv3 agent", + "HTTPS", + "Telnet"], value + ) + # Discovered service port: return as is + # Discovery status + if conditiontype == '10': + return to_numeric_value([ + "up", + "down", + "discovered", + "lost"], value + ) + if conditiontype == '13': + return self._zapi_wrapper.get_template_by_template_name(value)['templateid'] + if conditiontype == '18': + return self._zapi_wrapper.get_discovery_rule_by_discovery_rule_name(value)['druleid'] + if conditiontype == '19': + return self._zapi_wrapper.get_discovery_check_by_discovery_check_name(value)['dcheckid'] + if conditiontype == '20': + return self._zapi_wrapper.get_proxy_by_proxy_name(value)['proxyid'] + if conditiontype == '21': + return to_numeric_value([ + "pchldrfor0", + "host", + "service"], value + ) + if conditiontype == '23': + return to_numeric_value([ + "item in not supported state", + "item in normal state", + "LLD rule in not supported state", + "LLD rule in normal state", + "trigger in unknown state", + "trigger in normal state"], value + ) + return value + except Exception as e: + self._module.fail_json( + msg="""Unsupported value '%s' for specified condition type. + Check out Zabbix API documetation for supported values for + condition type '%s' at + https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition""" % (value, conditiontype) + ) + + def construct_the_data(self, _formula, _conditions): + """Construct the user defined filter conditions to fit the Zabbix API + requirements operations data using helper methods. + + Args: + _formula: zabbix condition evaluation formula + _conditions: conditions to construct + + Returns: + dict: user defined filter conditions + """ + if _conditions is None: + return None + constructed_data = {} + constructed_data['conditions'] = [] + for cond in _conditions: + condition_type = self._construct_conditiontype(cond) + constructed_data['conditions'].append({ + "conditiontype": condition_type, + "value": self._construct_value(condition_type, cond.get("value")), + "value2": cond.get("value2"), + "formulaid": cond.get("formulaid"), + "operator": self._construct_operator(cond) + }) + _constructed_evaltype = self._construct_evaltype( + _formula, + constructed_data['conditions'] + ) + constructed_data['evaltype'] = _constructed_evaltype['evaltype'] + constructed_data['formula'] = _constructed_evaltype['formula'] + return cleanup_data(constructed_data) + + +def convert_unicode_to_str(data): + """Converts unicode objects to strings in dictionary + args: + data: unicode object + + Returns: + dict: strings in dictionary + """ + if isinstance(data, dict): + return dict(map(convert_unicode_to_str, data.items())) + elif isinstance(data, (list, tuple, set)): + return type(data)(map(convert_unicode_to_str, data)) + elif data is None: + return data + else: + return str(data) + + +def to_numeric_value(strs, value): + """Converts string values to integers + Args: + value: string value + + Returns: + int: converted integer + """ + strs = [s.lower() if isinstance(s, str) else s for s in strs] + value = value.lower() + tmp_dict = dict(zip(strs, list(range(len(strs))))) + return str(tmp_dict[value]) + + +def compare_lists(l1, l2, diff_dict): + """ + Compares l1 and l2 lists and adds the items that are different + to the diff_dict dictionary. + Used in recursion with compare_dictionaries() function. + Args: + l1: first list to compare + l2: second list to compare + diff_dict: dictionary to store the difference + + Returns: + dict: items that are different + """ + if len(l1) != len(l2): + diff_dict.append(l1) + return diff_dict + for i, item in enumerate(l1): + if isinstance(item, dict): + diff_dict.insert(i, {}) + diff_dict[i] = compare_dictionaries(item, l2[i], diff_dict[i]) + else: + if item != l2[i]: + diff_dict.append(item) + while {} in diff_dict: + diff_dict.remove({}) + return diff_dict + + +def compare_dictionaries(d1, d2, diff_dict): + """ + Compares d1 and d2 dictionaries and adds the items that are different + to the diff_dict dictionary. + Used in recursion with compare_lists() function. + Args: + d1: first dictionary to compare + d2: second ditionary to compare + diff_dict: dictionary to store the difference + + Returns: + dict: items that are different + """ + for k, v in d1.items(): + if k not in d2: + diff_dict[k] = v + continue + if isinstance(v, dict): + diff_dict[k] = {} + compare_dictionaries(v, d2[k], diff_dict[k]) + if diff_dict[k] == {}: + del diff_dict[k] + else: + diff_dict[k] = v + elif isinstance(v, list): + diff_dict[k] = [] + compare_lists(v, d2[k], diff_dict[k]) + if diff_dict[k] == []: + del diff_dict[k] + else: + diff_dict[k] = v + else: + if v != d2[k]: + diff_dict[k] = v + return diff_dict + + +def cleanup_data(obj): + """Removes the None values from the object and returns the object + Args: + obj: object to cleanup + + Returns: + object: cleaned object + """ + if isinstance(obj, (list, tuple, set)): + return type(obj)(cleanup_data(x) for x in obj if x is not None) + elif isinstance(obj, dict): + return type(obj)((cleanup_data(k), cleanup_data(v)) + for k, v in obj.items() if k is not None and v is not None) + else: + return obj + + +def main(): + """Main ansible module function + """ + 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), + validate_certs=dict(type='bool', required=False, default=True), + esc_period=dict(type='int', required=False, default=60), + timeout=dict(type='int', default=10), + name=dict(type='str', required=True), + event_source=dict(type='str', required=True, choices=['trigger', 'discovery', 'auto_registration', 'internal']), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + status=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']), + default_message=dict(type='str', required=False, default=None), + default_subject=dict(type='str', required=False, default=None), + recovery_default_message=dict(type='str', required=False, default=None), + recovery_default_subject=dict(type='str', required=False, default=None), + acknowledge_default_message=dict(type='str', required=False, default=None), + acknowledge_default_subject=dict(type='str', required=False, default=None), + conditions=dict(type='list', required=False, default=None), + formula=dict(type='str', required=False, default=None), + operations=dict(type='list', required=False, default=None), + recovery_operations=dict(type='list', required=False, default=None), + acknowledge_operations=dict(type='list', required=False, default=None) + ), + 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)") + + 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'] + validate_certs = module.params['validate_certs'] + timeout = module.params['timeout'] + name = module.params['name'] + esc_period = module.params['esc_period'] + event_source = module.params['event_source'] + state = module.params['state'] + status = module.params['status'] + default_message = module.params['default_message'] + default_subject = module.params['default_subject'] + recovery_default_message = module.params['recovery_default_message'] + recovery_default_subject = module.params['recovery_default_subject'] + acknowledge_default_message = module.params['acknowledge_default_message'] + acknowledge_default_subject = module.params['acknowledge_default_subject'] + conditions = module.params['conditions'] + formula = module.params['formula'] + operations = module.params['operations'] + recovery_operations = module.params['recovery_operations'] + acknowledge_operations = module.params['acknowledge_operations'] + + 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) + + zapi_wrapper = Zapi(module, zbx) + + action = Action(module, zbx, zapi_wrapper) + + action_exists = zapi_wrapper.check_if_action_exists(name) + ops = Operations(module, zbx, zapi_wrapper) + recovery_ops = RecoveryOperations(module, zbx, zapi_wrapper) + acknowledge_ops = AcknowledgeOperations(module, zbx, zapi_wrapper) + fltr = Filter(module, zbx, zapi_wrapper) + + if action_exists: + action_id = zapi_wrapper.get_action_by_name(name)['actionid'] + if state == "absent": + result = action.delete_action(action_id) + module.exit_json(changed=True, msg="Action Deleted: %s, ID: %s" % (name, result)) + else: + difference = action.check_difference( + action_id=action_id, + name=name, + event_source=event_source, + esc_period=esc_period, + status=status, + default_message=default_message, + default_subject=default_subject, + recovery_default_message=recovery_default_message, + recovery_default_subject=recovery_default_subject, + acknowledge_default_message=acknowledge_default_message, + acknowledge_default_subject=acknowledge_default_subject, + operations=ops.construct_the_data(operations), + recovery_operations=recovery_ops.construct_the_data(recovery_operations), + acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations), + conditions=fltr.construct_the_data(formula, conditions) + ) + + if difference == {}: + module.exit_json(changed=False, msg="Action is up to date: %s" % (name)) + else: + result = action.update_action( + action_id=action_id, + **difference + ) + module.exit_json(changed=True, msg="Action Updated: %s, ID: %s" % (name, result)) + else: + if state == "absent": + module.exit_json(changed=False) + else: + action_id = action.add_action( + name=name, + event_source=event_source, + esc_period=esc_period, + status=status, + default_message=default_message, + default_subject=default_subject, + recovery_default_message=recovery_default_message, + recovery_default_subject=recovery_default_subject, + acknowledge_default_message=acknowledge_default_message, + acknowledge_default_subject=acknowledge_default_subject, + operations=ops.construct_the_data(operations), + recovery_operations=recovery_ops.construct_the_data(recovery_operations), + acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations), + conditions=fltr.construct_the_data(formula, conditions) + ) + module.exit_json(changed=True, msg="Action created: %s, ID: %s" % (name, action_id)) + + +if __name__ == '__main__': + main()