#!/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: - 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 option 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). - When I(type) is set to C(trigger_severity), the choices are (case-insensitive) C(not classified), C(information), C(warning), C(average), C(high), C(disaster) irrespective of user-visible names being changed in Zabbix. Defaults to C(not classified) if omitted. - Besides the above options, this is usualy either the name of the object or a string to compare with. operator: description: - Condition operator. - When I(type) is set to C(time_period), the choices are C(in), C(not in). 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 upper-case letters. - Required for custom expression filters. eval_type: description: - Filter condition evaluation method. - Defaults to C(andor) if conditions are less then 2 or if I(formula) is not specified. - Defaults to C(custom_expression) when formula is specified. choices: - 'andor' - 'and' - 'or' - 'custom_expression' 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. host_groups: type: list description: - List of host groups host should be added to. - Required when I(type=add_to_host_group) or I(type=remove_from_host_group). templates: type: list description: - List of templates host should be linked to. - Required when I(type=link_to_template) or I(type=unlink_from_template). inventory: description: - Host inventory mode. - Required when I(type=set_host_inventory_mode). 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. - If set to 0 the command will be run on the current host. 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 for I(operations). - Works only with >= Zabbix 3.2 acknowledge_operations: type: list description: - List of acknowledge operations. - C(Suboptions) are the same as for I(operations). - Works only with >= Zabbix 3.4 notes: - Only Zabbix >= 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 acknowledge 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: str 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)) 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 operation 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 operation 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_type, _formula, _conditions): """Construct the eval type Args: _formula: 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 } if _eval_type == 'andor': return { 'evaltype': '0', 'formula': None } if _eval_type == 'and': return { 'evaltype': '1', 'formula': None } if _eval_type == 'or': return { 'evaltype': '2', 'formula': None } if _eval_type == 'custom_expression': if _formula is not None: return { 'evaltype': '3', 'formula': _formula } else: self._module.fail_json(msg="'formula' is required when 'eval_type' is set to 'custom_expression'") if _formula is not None: return { 'evaltype': '3', 'formula': _formula } return { 'evaltype': '0', 'formula': None } 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 documentation 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, _eval_type, _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( _eval_type, _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), eval_type=dict(type='str', required=False, default=None, choices=['andor', 'and', 'or', 'custom_expression']), operations=dict(type='list', required=False, default=None), recovery_operations=dict(type='list', required=False, default=[]), acknowledge_operations=dict(type='list', required=False, default=[]) ), 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'] eval_type = module.params['eval_type'] 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(eval_type, 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(eval_type, formula, conditions) ) module.exit_json(changed=True, msg="Action created: %s, ID: %s" % (name, action_id)) if __name__ == '__main__': main()