diff --git a/doc/ref/states/all/salt.states.zabbix_action.rst b/doc/ref/states/all/salt.states.zabbix_action.rst new file mode 100644 index 0000000000..2f3999a888 --- /dev/null +++ b/doc/ref/states/all/salt.states.zabbix_action.rst @@ -0,0 +1,5 @@ +salt.states.zabbix_action module +============================== + +.. automodule:: salt.states.zabbix_action + :members: diff --git a/doc/ref/states/all/salt.states.zabbix_template.rst b/doc/ref/states/all/salt.states.zabbix_template.rst new file mode 100644 index 0000000000..e2e1ea62d7 --- /dev/null +++ b/doc/ref/states/all/salt.states.zabbix_template.rst @@ -0,0 +1,5 @@ +salt.states.zabbix_template module +============================== + +.. automodule:: salt.states.zabbix_template + :members: diff --git a/doc/ref/states/all/salt.states.zabbix_valuemap.rst b/doc/ref/states/all/salt.states.zabbix_valuemap.rst new file mode 100644 index 0000000000..5c033ac5d9 --- /dev/null +++ b/doc/ref/states/all/salt.states.zabbix_valuemap.rst @@ -0,0 +1,5 @@ +salt.states.zabbix_valuemap module +============================== + +.. automodule:: salt.states.zabbix_valuemap + :members: diff --git a/salt/modules/zabbix.py b/salt/modules/zabbix.py index 9a9d361d20..3696e033ef 100644 --- a/salt/modules/zabbix.py +++ b/salt/modules/zabbix.py @@ -23,34 +23,91 @@ Support for Zabbix :codeauthor: Jiri Kotlin ''' -from __future__ import absolute_import, print_function, unicode_literals +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + # Import python libs import logging import socket +import json +import os # Import salt libs -from salt.ext import six -import salt.utils.http -import salt.utils.json -import salt.utils.path -from salt.utils.versions import LooseVersion as _LooseVersion -from salt.ext.six.moves.urllib.error import HTTPError, URLError # pylint: disable=import-error,no-name-in-module +try: + import salt.utils + from salt.ext import six + from salt.utils.versions import LooseVersion as _LooseVersion + # pylint: disable=import-error,no-name-in-module,unused-import + from salt.ext.six.moves.urllib.error import HTTPError, URLError + from salt.exceptions import SaltException + IMPORTS_OK = True +except ImportError: + IMPORTS_OK = False log = logging.getLogger(__name__) INTERFACE_DEFAULT_PORTS = [10050, 161, 623, 12345] +ZABBIX_TOP_LEVEL_OBJECTS = ('hostgroup', 'template', 'host', 'maintenance', 'action', 'drule', 'service', 'proxy', + 'screen', 'usergroup', 'mediatype', 'script', 'valuemap') + +# Zabbix object and its ID name mapping +ZABBIX_ID_MAPPER = { + 'action': 'actionid', + 'alert': 'alertid', + 'application': 'applicationid', + 'dhost': 'dhostid', + 'dservice': 'dserviceid', + 'dcheck': 'dcheckid', + 'drule': 'druleid', + 'event': 'eventid', + 'graph': 'graphid', + 'graphitem': 'gitemid', + 'graphprototype': 'graphid', + 'history': 'itemid', + 'host': 'hostid', + 'hostgroup': 'groupid', + 'hostinterface': 'interfaceid', + 'hostprototype': 'hostid', + 'iconmap': 'iconmapid', + 'image': 'imageid', + 'item': 'itemid', + 'itemprototype': 'itemid', + 'service': 'serviceid', + 'discoveryrule': 'itemid', + 'maintenance': 'maintenanceid', + 'map': 'sysmapid', + 'usermedia': 'mediaid', + 'mediatype': 'mediatypeid', + 'proxy': 'proxyid', + 'screen': 'screenid', + 'screenitem': 'screenitemid', + 'script': 'scriptid', + 'template': 'templateid', + 'templatescreen': 'screenid', + 'templatescreenitem': 'screenitemid', + 'trend': 'itemid', + 'trigger': 'triggerid', + 'triggerprototype': 'triggerid', + 'user': 'userid', + 'usergroup': 'usrgrpid', + 'usermacro': 'globalmacroid', + 'valuemap': 'valuemapid', + 'httptest': 'httptestid' +} + # Define the module's virtual name __virtualname__ = 'zabbix' def __virtual__(): ''' - Only load the module if Zabbix server is installed + Only load the module if all modules are imported correctly. ''' - if salt.utils.path.which('zabbix_server'): + if IMPORTS_OK: return __virtualname__ - return (False, 'The zabbix execution module cannot be loaded: zabbix not installed.') + return False, 'Importing modules failed.' def _frontend_url(): @@ -86,7 +143,9 @@ def _query(method, params, url, auth=None): :param url: url of zabbix api :param auth: auth token for zabbix api (only for methods with required authentication) - :return: Response from API with desired data in JSON format. + :return: Response from API with desired data in JSON format. In case of error returns more specific description. + + .. versionchanged:: 2017.7 ''' unauthenticated_methods = ['user.login', 'apiinfo.version', ] @@ -99,17 +158,28 @@ def _query(method, params, url, auth=None): data = salt.utils.json.dumps(data) + log.info('_QUERY input:\nurl: %s\ndata: %s', six.text_type(url), six.text_type(data)) + try: result = salt.utils.http.query(url, method='POST', data=data, header_dict=header_dict, decode_type='json', - decode=True,) + decode=True, + status=True, + headers=True) + log.info('_QUERY result: %s', six.text_type(result)) + if 'error' in result: + raise SaltException('Zabbix API: Status: {0} ({1})'.format(result['status'], result['error'])) ret = result.get('dict', {}) + if 'error' in ret: + raise SaltException('Zabbix API: {} ({})'.format(ret['error']['message'], ret['error']['data'])) return ret - except (URLError, socket.gaierror): - return {} + except ValueError as err: + raise SaltException('URL or HTTP headers are probably not correct! ({})'.format(err)) + except socket.error as err: + raise SaltException('Check hostname in URL! ({})'.format(err)) def _login(**kwargs): @@ -171,8 +241,8 @@ def _login(**kwargs): return connargs else: raise KeyError - except KeyError: - return False + except KeyError as err: + raise SaltException('URL is probably not correct! ({})'.format(err)) def _params_extend(params, _ignore_name=False, **kwargs): @@ -208,6 +278,160 @@ def _params_extend(params, _ignore_name=False, **kwargs): return params +def get_zabbix_id_mapper(): + ''' + .. versionadded:: 2017.7 + + Make ZABBIX_ID_MAPPER constant available to state modules. + + :return: ZABBIX_ID_MAPPER + ''' + return ZABBIX_ID_MAPPER + + +def substitute_params(input_object, extend_params=None, filter_key='name', **kwargs): + ''' + .. versionadded:: 2017.7 + + Go through Zabbix object params specification and if needed get given object ID from Zabbix API and put it back + as a value. Definition of the object is done via dict with keys "query_object" and "query_name". + + :param input_object: Zabbix object type specified in state file + :param extend_params: Specify query with params + :param filter_key: Custom filtering key (default: name) + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + :return: Params structure with values converted to string for further comparison purposes + ''' + if extend_params is None: + extend_params = {} + if isinstance(input_object, list): + return [substitute_params(oitem, extend_params, filter_key, **kwargs) for oitem in input_object] + elif isinstance(input_object, dict): + if 'query_object' in input_object: + query_params = {} + if input_object['query_object'] not in ZABBIX_TOP_LEVEL_OBJECTS: + query_params.update(extend_params) + try: + query_params.update({'filter': {filter_key: input_object['query_name']}}) + return get_object_id_by_params(input_object['query_object'], query_params, **kwargs) + except KeyError: + raise SaltException('Qyerying object ID requested ' + 'but object name not provided: {0}'.format(input_object)) + else: + return {key: substitute_params(val, extend_params, filter_key, **kwargs) + for key, val in input_object.items()} + else: + # Zabbix response is always str, return everything in str as well + return six.text_type(input_object) + + +# pylint: disable=too-many-return-statements +def compare_params(defined, existing, return_old_value=False): + ''' + .. versionadded:: 2017.7 + + Compares Zabbix object definition against existing Zabbix object. + + :param defined: Zabbix object definition taken from sls file. + :param existing: Existing Zabbix object taken from result of an API call. + :param return_old_value: Default False. If True, returns dict("old"=old_val, "new"=new_val) for rollback purpose. + :return: Params that are different from existing object. Result extended by object ID can be passed directly to + Zabbix API update method. + ''' + # Comparison of data types + if not isinstance(defined, type(existing)): + raise SaltException('Zabbix object comparison failed (data type mismatch). Expecting {0}, got {1}. ' + 'Existing value: "{2}", defined value: "{3}").'.format(type(existing), + type(defined), + existing, + defined)) + + # Comparison of values + if not salt.utils.is_iter(defined): + if six.text_type(defined) != six.text_type(existing) and return_old_value: + return {'new': six.text_type(defined), 'old': six.text_type(existing)} + elif six.text_type(defined) != six.text_type(existing) and not return_old_value: + return six.text_type(defined) + + # Comparison of lists of values or lists of dicts + if isinstance(defined, list): + if len(defined) != len(existing): + log.info('Different list length!') + return {'new': defined, 'old': existing} if return_old_value else defined + else: + difflist = [] + for ditem in defined: + d_in_e = [] + for eitem in existing: + comp = compare_params(ditem, eitem, return_old_value) + if return_old_value: + d_in_e.append(comp['new']) + else: + d_in_e.append(comp) + if all(d_in_e): + difflist.append(ditem) + # If there is any difference in a list then whole defined list must be returned and provided for update + if any(difflist) and return_old_value: + return {'new': defined, 'old': existing} + elif any(difflist) and not return_old_value: + return defined + + # Comparison of dicts + if isinstance(defined, dict): + try: + # defined must be a subset of existing to be compared + if set(defined) <= set(existing): + intersection = set(defined) & set(existing) + diffdict = {'new': {}, 'old': {}} if return_old_value else {} + for i in intersection: + comp = compare_params(defined[i], existing[i], return_old_value) + if return_old_value: + if comp or (not comp and isinstance(comp, list)): + diffdict['new'].update({i: defined[i]}) + diffdict['old'].update({i: existing[i]}) + else: + if comp or (not comp and isinstance(comp, list)): + diffdict.update({i: defined[i]}) + return diffdict + + return {'new': defined, 'old': existing} if return_old_value else defined + + except TypeError: + raise SaltException('Zabbix object comparison failed (data type mismatch). Expecting {0}, got {1}. ' + 'Existing value: "{2}", defined value: "{3}").'.format(type(existing), + type(defined), + existing, + defined)) + + +def get_object_id_by_params(obj, params=None, **connection_args): + ''' + .. versionadded:: 2017.7 + + Get ID of single Zabbix object specified by its name. + + :param obj: Zabbix object type + :param params: Parameters by which object is uniquely identified + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + :return: object ID + ''' + if params is None: + params = {} + res = run_query(obj + '.get', params, **connection_args) + if res and len(res) == 1: + return six.text_type(res[0][ZABBIX_ID_MAPPER[obj]]) + else: + raise SaltException('Zabbix API: Object does not exist or bad Zabbix user permissions or other unexpected ' + 'result. Called method {0} with params {1}. ' + 'Result: {2}'.format(obj + '.get', params, res)) + + def apiinfo_version(**connection_args): ''' Retrieve the version of the Zabbix API. @@ -846,8 +1070,8 @@ def host_create(host, groups, interfaces, **connection_args): .. code-block:: bash salt '*' zabbix.host_create technicalname 4 - interfaces='{type: 1, main: 1, useip: 1, ip: "192.168.3.1", dns: "", port: 10050}' - visible_name='Host Visible Name' inventory_mode=0 inventory='{"alias": "something"}' + interfaces='{if_type: 1, main: 1, useip: 1, ip_: "192.168.3.1", dns: "", port: 10050}' + visible_name='Host Visible Name' ''' conn_args = _login(**connection_args) ret = False @@ -1466,7 +1690,7 @@ def hostinterface_get(hostids, **connection_args): return ret -def hostinterface_create(hostid, ip, dns='', main=1, type=1, useip=1, port=None, **connection_args): +def hostinterface_create(hostid, ip_, dns='', main=1, if_type=1, useip=1, port=None, **connection_args): ''' Create new host interface NOTE: This function accepts all standard host group interface: keyword argument names differ depending @@ -1475,11 +1699,11 @@ def hostinterface_create(hostid, ip, dns='', main=1, type=1, useip=1, port=None, .. versionadded:: 2016.3.0 :param hostid: ID of the host the interface belongs to - :param ip: IP address used by the interface + :param ip_: IP address used by the interface :param dns: DNS name used by the interface :param main: whether the interface is used as default on the host (0 - not default, 1 - default) :param port: port number used by the interface - :param type: Interface type (1 - agent; 2 - SNMP; 3 - IPMI; 4 - JMX) + :param if_type: Interface type (1 - agent; 2 - SNMP; 3 - IPMI; 4 - JMX) :param useip: Whether the connection should be made via IP (0 - connect using host DNS name; 1 - connect using host IP address for this host interface) :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) @@ -1497,12 +1721,18 @@ def hostinterface_create(hostid, ip, dns='', main=1, type=1, useip=1, port=None, ret = False if not port: - port = INTERFACE_DEFAULT_PORTS[type] + port = INTERFACE_DEFAULT_PORTS[if_type] try: if conn_args: method = 'hostinterface.create' - params = {"hostid": hostid, "ip": ip, "dns": dns, "main": main, "port": port, "type": type, "useip": useip} + params = {"hostid": hostid, + "ip": ip_, + "dns": dns, + "main": main, + "port": port, + "type": if_type, + "useip": useip} params = _params_extend(params, **connection_args) ret = _query(method, params, conn_args['url'], conn_args['auth']) return ret['result']['interfaceids'] @@ -1565,7 +1795,7 @@ def hostinterface_update(interfaceid, **connection_args): CLI Example: .. code-block:: bash - salt '*' zabbix.hostinterface_update 6 ip=0.0.0.2 + salt '*' zabbix.hostinterface_update 6 ip_=0.0.0.2 ''' conn_args = _login(**connection_args) ret = False @@ -1879,8 +2109,9 @@ def mediatype_get(name=None, mediatypeids=None, **connection_args): _connection_password: zabbix password (can also be set in opts or pillar, see module's docstring) _connection_url: url of zabbix frontend (can also be set in opts or pillar, see module's docstring) - all optional mediatype.get parameters: keyword argument names differ depending on your zabbix - version,nsee: https://www.zabbix.com/documentation/2.2/manual/api/reference/mediatype/get + all optional mediatype.get parameters: keyword argument names depends on your zabbix version, see: + + https://www.zabbix.com/documentation/2.2/manual/api/reference/mediatype/get Returns: Array with mediatype details, False if no mediatype found or on failure. @@ -2039,8 +2270,9 @@ def template_get(name=None, host=None, templateids=None, **connection_args): _connection_password: zabbix password (can also be set in opts or pillar, see module's docstring) _connection_url: url of zabbix frontend (can also be set in opts or pillar, see module's docstring) - all optional template.get parameters: keyword argument names differ depending on your zabbix - version, see: https://www.zabbix.com/documentation/2.4/manual/api/reference/template/get + all optional template.get parameters: keyword argument names depends on your zabbix version, see: + + https://www.zabbix.com/documentation/2.4/manual/api/reference/template/get Returns: Array with convenient template details, False if no template found or on failure. @@ -2085,8 +2317,9 @@ def run_query(method, params, **connection_args): _connection_password: zabbix password (can also be set in opts or pillar, see module's docstring) _connection_url: url of zabbix frontend (can also be set in opts or pillar, see module's docstring) - all optional template.get parameters: keyword argument names differ depending on your zabbix - version, see: https://www.zabbix.com/documentation/2.4/manual/api/reference/ + all optional template.get parameters: keyword argument names depends on your zabbix version, see: + + https://www.zabbix.com/documentation/2.4/manual/api/reference/ Returns: Response from Zabbix API @@ -2100,10 +2333,86 @@ def run_query(method, params, **connection_args): ret = False try: if conn_args: + method = method + params = params params = _params_extend(params, **connection_args) ret = _query(method, params, conn_args['url'], conn_args['auth']) + if isinstance(ret['result'], bool): + return ret['result'] return ret['result'] if len(ret['result']) > 0 else False else: raise KeyError except KeyError: return ret + + +def configuration_import(config_file, rules=None, file_format='xml', **connection_args): + ''' + .. versionadded:: 2017.7 + + Imports Zabbix configuration sepcified in file to Zabbix server. + + :param config_file: File with Zabbix config (local or remote) + :param rules: Optional - Rules that have to be different from default (defaults are the same as in Zabbix web UI.) + :param file_format: Config file format (default: xml) + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + CLI Example: + + .. code-block:: bash + + salt '*' zabbix.configuration_import salt://zabbix/config/zabbix_templates.xml \ + "{'screens': {'createMissing': True, 'updateExisting': True}}" + ''' + if rules is None: + rules = {} + default_rules = {'applications': {'createMissing': True, 'updateExisting': False, 'deleteMissing': False}, + 'discoveryRules': {'createMissing': True, 'updateExisting': True, 'deleteMissing': False}, + 'graphs': {'createMissing': True, 'updateExisting': True, 'deleteMissing': False}, + 'groups': {'createMissing': True}, + 'hosts': {'createMissing': False, 'updateExisting': False}, + 'images': {'createMissing': False, 'updateExisting': False}, + 'items': {'createMissing': True, 'updateExisting': True, 'deleteMissing': False}, + 'maps': {'createMissing': False, 'updateExisting': False}, + 'screens': {'createMissing': False, 'updateExisting': False}, + 'templateLinkage': {'createMissing': True}, + 'templates': {'createMissing': True, 'updateExisting': True}, + 'templateScreens': {'createMissing': True, 'updateExisting': True, 'deleteMissing': False}, + 'triggers': {'createMissing': True, 'updateExisting': True, 'deleteMissing': False}, + 'valueMaps': {'createMissing': True, 'updateExisting': False}} + new_rules = dict(default_rules) + + if rules: + for rule in rules: + if rule in new_rules: + new_rules[rule].update(rules[rule]) + else: + new_rules[rule] = rules[rule] + if 'salt://' in config_file: + tmpfile = salt.utils.mkstemp() + cfile = __salt__['cp.get_file'](config_file, tmpfile) + if not cfile or os.path.getsize(cfile) == 0: + return {'name': config_file, 'result': False, 'message': 'Failed to fetch config file.'} + else: + cfile = config_file + if not os.path.isfile(cfile): + return {'name': config_file, 'result': False, 'message': 'Invalid file path.'} + + with salt.utils.fopen(cfile, mode='r') as fp_: + xml = fp_.read() + + if 'salt://' in config_file: + salt.utils.safe_rm(cfile) + + params = {'format': file_format, + 'rules': new_rules, + 'source': xml} + log.info('CONFIGURATION IMPORT: rules: %s', six.text_type(params['rules'])) + try: + run_query('configuration.import', params, **connection_args) + return {'name': config_file, 'result': True, 'message': 'Zabbix API "configuration.import" method ' + 'called successfully.'} + except SaltException as exc: + return {'name': config_file, 'result': False, 'message': six.text_type(exc)} diff --git a/salt/states/zabbix_action.py b/salt/states/zabbix_action.py new file mode 100644 index 0000000000..27f010686c --- /dev/null +++ b/salt/states/zabbix_action.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +''' +.. versionadded:: 2017.7 + +Management of Zabbix Action object over Zabbix API. + +:codeauthor: Jakub Sliva +''' +from __future__ import absolute_import +from __future__ import unicode_literals +import logging +import json + +try: + from salt.ext import six + from salt.exceptions import SaltException + IMPORTS_OK = True +except ImportError: + IMPORTS_OK = False + + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only make these states available if Zabbix module and run_query function is available + and all 3rd party modules imported. + ''' + if 'zabbix.run_query' in __salt__ and IMPORTS_OK: + return True + return False, 'Import zabbix or other needed modules failed.' + + +def present(name, params, **kwargs): + ''' + Creates Zabbix Action object or if differs update it according defined parameters + + :param name: Zabbix Action name + :param params: Definition of the Zabbix Action + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + If there is a need to get a value from current zabbix online (e.g. id of a hostgroup you want to put a discovered + system into), put a dictionary with two keys "query_object" and "query_name" instead of the value. + In this example we want to get object id of hostgroup named "Virtual machines" and "Databases". + + .. code-block:: yaml + + zabbix-action-present: + zabbix_action.present: + - name: VMs + - params: + eventsource: 2 + status: 0 + filter: + evaltype: 2 + conditions: + - conditiontype: 24 + operator: 2 + value: 'virtual' + - conditiontype: 24 + operator: 2 + value: 'kvm' + operations: + - operationtype: 2 + - operationtype: 4 + opgroup: + - groupid: + query_object: hostgroup + query_name: Virtual machines + - groupid: + query_object: hostgroup + query_name: Databases + ''' + zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']() + + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + # Create input params substituting functions with their results + params['name'] = name + params['operations'] = params['operations'] if 'operations' in params else [] + if 'filter' in params: + params['filter']['conditions'] = params['filter']['conditions'] if 'conditions' in params['filter'] else [] + + input_params = __salt__['zabbix.substitute_params'](params, **kwargs) + log.info('Zabbix Action: input params: %s', six.text_type(json.dumps(input_params, indent=4))) + + search = {'output': 'extend', + 'selectOperations': 'extend', + 'selectFilter': 'extend', + 'filter': { + 'name': name + }} + # GET Action object if exists + action_get = __salt__['zabbix.run_query']('action.get', search, **kwargs) + log.info('Zabbix Action: action.get result: %s', six.text_type(json.dumps(action_get, indent=4))) + + existing_obj = __salt__['zabbix.substitute_params'](action_get[0], **kwargs) \ + if action_get and len(action_get) == 1 else False + + if existing_obj: + diff_params = __salt__['zabbix.compare_params'](input_params, existing_obj) + log.info('Zabbix Action: input params: {%s', six.text_type(json.dumps(input_params, indent=4))) + log.info('Zabbix Action: Object comparison result. Differences: %s', six.text_type(diff_params)) + + if diff_params: + diff_params[zabbix_id_mapper['action']] = existing_obj[zabbix_id_mapper['action']] + # diff_params['name'] = 'VMs' - BUG - https://support.zabbix.com/browse/ZBX-12078 + log.info('Zabbix Action: update params: %s', six.text_type(json.dumps(diff_params, indent=4))) + + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" would be fixed.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" differs ' + 'in following parameters: {1}'.format(name, diff_params), + 'new': 'Zabbix Action "{0}" would correspond to definition.'.format(name)}} + else: + action_update = __salt__['zabbix.run_query']('action.update', diff_params, **kwargs) + log.info('Zabbix Action: action.update result: %s', six.text_type(action_update)) + if action_update: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" differed ' + 'in following parameters: {1}'.format(name, diff_params), + 'new': 'Zabbix Action "{0}" fixed.'.format(name)}} + + else: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" already exists and corresponds to a definition.'.format(name) + + else: + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" would be created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" does not exist.'.format(name), + 'new': 'Zabbix Action "{0}" would be created according definition.'.format(name)}} + else: + # ACTION.CREATE + action_create = __salt__['zabbix.run_query']('action.create', input_params, **kwargs) + log.info('Zabbix Action: action.create result: ' + six.text_type(action_create)) + + if action_create: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" did not exist.'.format(name), + 'new': 'Zabbix Action "{0}" created according definition.'.format(name)}} + + return ret + + +def absent(name, **kwargs): + ''' + Makes the Zabbix Action to be absent (either does not exist or delete it). + + :param name: Zabbix Action name + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + .. code-block:: yaml + + zabbix-action-absent: + zabbix_action.absent: + - name: Action name + ''' + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + try: + object_id = __salt__['zabbix.get_object_id_by_params']('action', {'filter': {'name': name}}, **kwargs) + except SaltException: + object_id = False + + if not object_id: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" does not exist.'.format(name) + else: + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" exists.'.format(name), + 'new': 'Zabbix Action "{0}" would be deleted.'.format(name)}} + else: + action_delete = __salt__['zabbix.run_query']('action.delete', [object_id], **kwargs) + + if action_delete: + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" existed.'.format(name), + 'new': 'Zabbix Action "{0}" deleted.'.format(name)}} + + return ret diff --git a/salt/states/zabbix_template.py b/salt/states/zabbix_template.py new file mode 100644 index 0000000000..d482c03d39 --- /dev/null +++ b/salt/states/zabbix_template.py @@ -0,0 +1,708 @@ +# -*- coding: utf-8 -*- +''' +.. versionadded:: 2017.7 + +Management of Zabbix Template object over Zabbix API. + +:codeauthor: Jakub Sliva +''' +from __future__ import absolute_import +from __future__ import unicode_literals +import logging +import json + +try: + from salt.ext import six + from salt.exceptions import SaltException + IMPORTS_OK = True +except ImportError: + IMPORTS_OK = False + + +log = logging.getLogger(__name__) + +TEMPLATE_RELATIONS = ['groups', 'hosts', 'macros'] +TEMPLATE_COMPONENT_ORDER = ('applications', + 'items', + 'triggers', + 'gitems', + 'graphs', + 'screens', + 'httpTests', + 'discoveries') +DISCOVERYRULE_COMPONENT_ORDER = ('itemprototypes', 'triggerprototypes', 'graphprototypes', 'hostprototypes') +TEMPLATE_COMPONENT_DEF = { + # 'component': {'qtype': 'component type to query', + # 'qidname': 'component id name', + # 'qselectpid': 'particular component selection attribute name (parent id name)', + # 'ptype': 'parent component type', + # 'pid': 'parent component id', + # 'pid_ref_name': 'component's creation reference name for parent id', + # 'res_id_name': 'jsonrpc modification call result key name of list of affected IDs'}, + # 'output': {'output': 'extend', 'selectApplications': 'extend', 'templated': 'true'}, + # 'inherited': 'attribute name for inheritance toggling', + # 'filter': 'child component unique identification attribute name', + 'applications': {'qtype': 'application', + 'qidname': 'applicationid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': 'hostid', + 'res_id_name': 'applicationids', + 'output': {'output': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': True, + 'filter': 'name', + 'ro_attrs': ['applicationid', 'flags', 'templateids']}, + 'items': {'qtype': 'item', + 'qidname': 'itemid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': 'hostid', + 'res_id_name': 'itemids', + 'output': {'output': 'extend', 'selectApplications': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['itemid', 'error', 'flags', 'lastclock', 'lastns', + 'lastvalue', 'prevvalue', 'state', 'templateid']}, + 'triggers': {'qtype': 'trigger', + 'qidname': 'triggerid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': None, + 'res_id_name': 'triggerids', + 'output': {'output': 'extend', 'selectDependencies': 'expand', + 'templated': 'true', 'expandExpression': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'description', + 'ro_attrs': ['error', 'flags', 'lastchange', 'state', 'templateid', 'value']}, + 'graphs': {'qtype': 'graph', + 'qidname': 'graphid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': None, + 'res_id_name': 'graphids', + 'output': {'output': 'extend', 'selectGraphItems': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['graphid', 'flags', 'templateid']}, + 'gitems': {'qtype': 'graphitem', + 'qidname': 'itemid', + 'qselectpid': 'graphids', + 'ptype': 'graph', + 'pid': 'graphid', + 'pid_ref_name': None, + 'res_id_name': None, + 'output': {'output': 'extend'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['gitemid']}, + # "Template screen" + 'screens': {'qtype': 'templatescreen', + 'qidname': 'screenid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': 'templateid', + 'res_id_name': 'screenids', + 'output': {'output': 'extend', 'selectUsers': 'extend', 'selectUserGroups': 'extend', + 'selectScreenItems': 'extend', 'noInheritance': 'true'}, + 'inherited': 'noInheritance', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['screenid']}, + # "LLD rule" + 'discoveries': {'qtype': 'discoveryrule', + 'qidname': 'itemid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': 'hostid', + 'res_id_name': 'itemids', + 'output': {'output': 'extend', 'selectFilter': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'key_', + 'ro_attrs': ['itemid', 'error', 'state', 'templateid']}, + # "Web scenario" + 'httpTests': {'qtype': 'httptest', + 'qidname': 'httptestid', + 'qselectpid': 'templateids', + 'ptype': 'template', + 'pid': 'templateid', + 'pid_ref_name': 'hostid', + 'res_id_name': 'httptestids', + 'output': {'output': 'extend', 'selectSteps': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['httptestid', 'nextcheck', 'templateid']}, + # discoveries => discoveryrule + 'itemprototypes': {'qtype': 'itemprototype', + 'qidname': 'itemid', + 'qselectpid': 'discoveryids', + 'ptype': 'discoveryrule', + 'pid': 'itemid', + 'pid_ref_name': 'ruleid', + # exception only in case of itemprototype - needs both parent ruleid and hostid + 'pid_ref_name2': 'hostid', + 'res_id_name': 'itemids', + 'output': {'output': 'extend', 'selectSteps': 'extend', 'selectApplications': 'extend', + 'templated': 'true'}, + 'adjust': False, + 'inherited': 'inherited', + 'filter': 'name', + 'ro_attrs': ['itemid', 'templateid']}, + 'triggerprototypes': {'qtype': 'triggerprototype', + 'qidname': 'triggerid', + 'qselectpid': 'discoveryids', + 'ptype': 'discoveryrule', + 'pid': 'itemid', + 'pid_ref_name': None, + 'res_id_name': 'triggerids', + 'output': {'output': 'extend', 'selectTags': 'extend', 'selectDependencies': 'extend', + 'templated': 'true', 'expandExpression': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'description', + 'ro_attrs': ['triggerid', 'templateid']}, + 'graphprototypes': {'qtype': 'graphprototype', + 'qidname': 'graphid', + 'qselectpid': 'discoveryids', + 'ptype': 'discoveryrule', + 'pid': 'itemid', + 'pid_ref_name': None, + 'res_id_name': 'graphids', + 'output': {'output': 'extend', 'selectGraphItems': 'extend', 'templated': 'true'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'name', + 'ro_attrs': ['graphid', 'templateid']}, + 'hostprototypes': {'qtype': 'hostprototype', + 'qidname': 'hostid', + 'qselectpid': 'discoveryids', + 'ptype': 'discoveryrule', + 'pid': 'itemid', + 'pid_ref_name': 'ruleid', + 'res_id_name': 'hostids', + 'output': {'output': 'extend', 'selectGroupLinks': 'expand', 'selectGroupPrototypes': 'expand', + 'selectTemplates': 'expand'}, + 'inherited': 'inherited', + 'adjust': False, + 'filter': 'host', + 'ro_attrs': ['hostid', 'templateid']} +} + +# CHANGE_STACK = [{'component': 'items', 'action': 'create', 'params': dict|list}] +CHANGE_STACK = [] + + +def __virtual__(): + ''' + Only make these states available if Zabbix module and run_query function is available + and all 3rd party modules imported. + ''' + if 'zabbix.run_query' in __salt__ and IMPORTS_OK: + return True + return False, 'Import zabbix or other needed modules failed.' + + +def _diff_and_merge_host_list(defined, existing): + ''' + If Zabbix template is to be updated then list of assigned hosts must be provided in all or nothing manner to prevent + some externally assigned hosts to be detached. + + :param defined: list of hosts defined in sls + :param existing: list of hosts taken from live Zabbix + :return: list to be updated (combinated or empty list) + ''' + try: + defined_host_ids = set([host['hostid'] for host in defined]) + existing_host_ids = set([host['hostid'] for host in existing]) + except KeyError: + raise SaltException('List of hosts in template not defined correctly.') + + diff = defined_host_ids - existing_host_ids + return [{'hostid': six.text_type(hostid)} for hostid in diff | existing_host_ids] if diff else [] + + +def _get_existing_template_c_list(component, parent_id, **kwargs): + ''' + Make a list of given component type not inherited from other templates because Zabbix API returns only list of all + and list of inherited component items so we have to do a difference list. + + :param component: Template component (application, item, etc...) + :param parent_id: ID of existing template the component is assigned to + :return List of non-inherited (own) components + ''' + c_def = TEMPLATE_COMPONENT_DEF[component] + q_params = dict(c_def['output']) + q_params.update({c_def['qselectpid']: parent_id}) + + existing_clist_all = __salt__['zabbix.run_query'](c_def['qtype'] + '.get', q_params, **kwargs) + + # in some cases (e.g. templatescreens) the logic is reversed (even name of the flag is different!) + if c_def['inherited'] == 'inherited': + q_params.update({c_def['inherited']: 'true'}) + existing_clist_inherited = __salt__['zabbix.run_query'](c_def['qtype'] + '.get', q_params, **kwargs) + else: + existing_clist_inherited = [] + + if existing_clist_inherited: + return [c_all for c_all in existing_clist_all if c_all not in existing_clist_inherited] + + return existing_clist_all + + +def _adjust_object_lists(obj): + ''' + For creation or update of object that have attribute which contains a list Zabbix awaits plain list of IDs while + querying Zabbix for same object returns list of dicts + + :param obj: Zabbix object parameters + ''' + for subcomp in TEMPLATE_COMPONENT_DEF: + if subcomp in obj and TEMPLATE_COMPONENT_DEF[subcomp]['adjust']: + obj[subcomp] = [item[TEMPLATE_COMPONENT_DEF[subcomp]['qidname']] for item in obj[subcomp]] + + +def _manage_component(component, parent_id, defined, existing, template_id=None, **kwargs): + ''' + Takes particular component list, compares it with existing, call appropriate API methods - create, update, delete. + + :param component: component name + :param parent_id: ID of parent entity under which component should be created + :param defined: list of defined items of named component + :param existing: list of existing items of named component + :param template_id: In case that component need also template ID for creation (although parent_id is given?!?!?) + ''' + zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']() + + dry_run = __opts__['test'] + c_def = TEMPLATE_COMPONENT_DEF[component] + compare_key = c_def['filter'] + + defined_set = set([item[compare_key] for item in defined]) + existing_set = set([item[compare_key] for item in existing]) + + create_set = defined_set - existing_set + update_set = defined_set & existing_set + delete_set = existing_set - defined_set + + create_list = [item for item in defined if item[compare_key] in create_set] + for object_params in create_list: + if parent_id: + object_params.update({c_def['pid_ref_name']: parent_id}) + + if 'pid_ref_name2' in c_def: + object_params.update({c_def['pid_ref_name2']: template_id}) + + _adjust_object_lists(object_params) + + if not dry_run: + object_create = __salt__['zabbix.run_query'](c_def['qtype'] + '.create', object_params, **kwargs) + if object_create: + object_ids = object_create[c_def['res_id_name']] + CHANGE_STACK.append({'component': component, 'action': 'create', 'params': object_params, + c_def['filter']: object_params[c_def['filter']], 'object_id': object_ids}) + else: + CHANGE_STACK.append({'component': component, 'action': 'create', 'params': object_params, + 'object_id': 'CREATED '+TEMPLATE_COMPONENT_DEF[component]['qtype']+' ID'}) + + delete_list = [item for item in existing if item[compare_key] in delete_set] + for object_del in delete_list: + object_id_name = zabbix_id_mapper[c_def['qtype']] + CHANGE_STACK.append({'component': component, 'action': 'delete', 'params': [object_del[object_id_name]]}) + if not dry_run: + __salt__['zabbix.run_query'](c_def['qtype'] + '.delete', [object_del[object_id_name]], **kwargs) + + for object_name in update_set: + ditem = next((item for item in defined if item[compare_key] == object_name), None) + eitem = next((item for item in existing if item[compare_key] == object_name), None) + diff_params = __salt__['zabbix.compare_params'](ditem, eitem, True) + + if diff_params['new']: + diff_params['new'][zabbix_id_mapper[c_def['qtype']]] = eitem[zabbix_id_mapper[c_def['qtype']]] + diff_params['old'][zabbix_id_mapper[c_def['qtype']]] = eitem[zabbix_id_mapper[c_def['qtype']]] + _adjust_object_lists(diff_params['new']) + _adjust_object_lists(diff_params['old']) + CHANGE_STACK.append({'component': component, 'action': 'update', 'params': diff_params['new']}) + + if not dry_run: + __salt__['zabbix.run_query'](c_def['qtype'] + '.update', diff_params['new'], **kwargs) + + +# pylint: disable=too-many-statements,too-many-locals +def present(name, params, static_host_list=True, **kwargs): + ''' + Creates Zabbix Template object or if differs update it according defined parameters. See Zabbix API documentation. + + Zabbix API version: >3.0 + + :param name: Zabbix Template name + :param params: Additional parameters according to Zabbix API documentation + :param static_host_list: If hosts assigned to the template are controlled only by this state or can be also + assigned externally + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + .. note:: + + If there is a need to get a value from current zabbix online (e.g. ids of host groups you want the template + to be associated with), put a dictionary with two keys "query_object" and "query_name" instead of the value. + In this example we want to create template named "Testing Template", assign it to hostgroup Templates, + link it to two ceph nodes and create a macro. + + .. note:: + + IMPORTANT NOTE: + Objects (except for template name) are identified by name (or by other key in some exceptional cases) + so changing name of object means deleting old one and creating new one with new ID !!! + + .. note:: + + NOT SUPPORTED FEATURES: + - linked templates + - trigger dependencies + - groups and group prototypes for host prototypes + + SLS Example: + + .. code-block:: yaml + + zabbix-template-present: + zabbix_template.present: + - name: Testing Template + # Do not touch existing assigned hosts + # True will detach all other hosts than defined here + - static_host_list: False + - params: + description: Template for Ceph nodes + groups: + # groups must already exist + # template must be at least in one hostgroup + - groupid: + query_object: hostgroup + query_name: Templates + macros: + - macro: "{$CEPH_CLUSTER_NAME}" + value: ceph + hosts: + # hosts must already exist + - hostid: + query_object: host + query_name: ceph-osd-01 + - hostid: + query_object: host + query_name: ceph-osd-02 + # templates: + # Linked templates - not supported by state module but can be linked manually (will not be touched) + + applications: + - name: Ceph OSD + items: + - name: Ceph OSD avg fill item + key_: ceph.osd_avg_fill + type: 2 + value_type: 0 + delay: 60 + units: '%' + description: 'Average fill of OSD' + applications: + - applicationid: + query_object: application + query_name: Ceph OSD + triggers: + - description: "Ceph OSD filled more that 90%" + expression: "{{'{'}}Testing Template:ceph.osd_avg_fill.last(){{'}'}}>90" + priority: 4 + discoveries: + - name: Mounted filesystem discovery + key_: vfs.fs.discovery + type: 0 + delay: 60 + itemprototypes: + - name: Free disk space on {{'{#'}}FSNAME} + key_: vfs.fs.size[{{'{#'}}FSNAME},free] + type: 0 + value_type: 3 + delay: 60 + applications: + - applicationid: + query_object: application + query_name: Ceph OSD + triggerprototypes: + - description: "Free disk space is less than 20% on volume {{'{#'}}FSNAME{{'}'}}" + expression: "{{'{'}}Testing Template:vfs.fs.size[{{'{#'}}FSNAME},free].last(){{'}'}}<20" + graphs: + - name: Ceph OSD avg fill graph + width: 900 + height: 200 + graphtype: 0 + gitems: + - color: F63100 + itemid: + query_object: item + query_name: Ceph OSD avg fill item + screens: + - name: Ceph + hsize: 1 + vsize: 1 + screenitems: + - x: 0 + y: 0 + resourcetype: 0 + resourceid: + query_object: graph + query_name: Ceph OSD avg fill graph + ''' + zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']() + + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + params['host'] = name + + # Divide template yaml definition into parts + # - template definition itself + # - simple template components + # - components that have other sub-components + # (e.g. discoveries - where parent ID is needed in advance for sub-component manipulation) + template_definition = {} + template_components = {} + discovery_components = [] + + for attr in params: + if attr in TEMPLATE_COMPONENT_ORDER and six.text_type(attr) != 'discoveries': + template_components[attr] = params[attr] + + elif six.text_type(attr) == 'discoveries': + d_rules = [] + for d_rule in params[attr]: + d_rule_components = {'query_pid': {'component': attr, + 'filter_val': d_rule[TEMPLATE_COMPONENT_DEF[attr]['filter']]}} + for proto_name in DISCOVERYRULE_COMPONENT_ORDER: + if proto_name in d_rule: + d_rule_components[proto_name] = d_rule[proto_name] + del d_rule[proto_name] + + discovery_components.append(d_rule_components) + d_rules.append(d_rule) + + template_components[attr] = d_rules + + else: + template_definition[attr] = params[attr] + + # if a component is not defined, it means to remove existing items during update (empty list) + for attr in TEMPLATE_COMPONENT_ORDER: + if attr not in template_components: + template_components[attr] = [] + + # if a component is not defined, it means to remove existing items during update (empty list) + for attr in TEMPLATE_RELATIONS: + template_definition[attr] = params[attr] if attr in params and params[attr] else [] + + defined_obj = __salt__['zabbix.substitute_params'](template_definition, **kwargs) + log.info('SUBSTITUTED template_definition: %s', six.text_type(json.dumps(defined_obj, indent=4))) + + tmpl_get = __salt__['zabbix.run_query']('template.get', + {'output': 'extend', 'selectGroups': 'groupid', 'selectHosts': 'hostid', + 'selectTemplates': 'templateid', 'selectMacros': 'extend', + 'filter': {'host': name}}, + **kwargs) + log.info('TEMPLATE get result: %s', six.text_type(json.dumps(tmpl_get, indent=4))) + + existing_obj = __salt__['zabbix.substitute_params'](tmpl_get[0], **kwargs) \ + if tmpl_get and len(tmpl_get) == 1 else False + + if existing_obj: + template_id = existing_obj[zabbix_id_mapper['template']] + + if not static_host_list: + # Prepare objects for comparison + defined_wo_hosts = defined_obj + if 'hosts' in defined_obj: + defined_hosts = defined_obj['hosts'] + del defined_wo_hosts['hosts'] + else: + defined_hosts = [] + + existing_wo_hosts = existing_obj + if 'hosts' in existing_obj: + existing_hosts = existing_obj['hosts'] + del existing_wo_hosts['hosts'] + else: + existing_hosts = [] + + # Compare host list separately from the rest of the object comparison since the merged list is needed for + # update + hosts_list = _diff_and_merge_host_list(defined_hosts, existing_hosts) + + # Compare objects without hosts + diff_params = __salt__['zabbix.compare_params'](defined_wo_hosts, existing_wo_hosts, True) + + # Merge comparison results together + if ('new' in diff_params and 'hosts' in diff_params['new']) or hosts_list: + diff_params['new']['hosts'] = hosts_list + + else: + diff_params = __salt__['zabbix.compare_params'](defined_obj, existing_obj, True) + + if diff_params['new']: + diff_params['new'][zabbix_id_mapper['template']] = template_id + diff_params['old'][zabbix_id_mapper['template']] = template_id + log.info('TEMPLATE: update params: %s', six.text_type(json.dumps(diff_params, indent=4))) + + CHANGE_STACK.append({'component': 'template', 'action': 'update', 'params': diff_params['new']}) + if not dry_run: + tmpl_update = __salt__['zabbix.run_query']('template.update', diff_params['new'], **kwargs) + log.info('TEMPLATE update result: %s', six.text_type(tmpl_update)) + + else: + CHANGE_STACK.append({'component': 'template', 'action': 'create', 'params': defined_obj}) + if not dry_run: + tmpl_create = __salt__['zabbix.run_query']('template.create', defined_obj, **kwargs) + log.info('TEMPLATE create result: ' + six.text_type(tmpl_create)) + if tmpl_create: + template_id = tmpl_create['templateids'][0] + + log.info('\n\ntemplate_components: %s', json.dumps(template_components, indent=4)) + log.info('\n\ndiscovery_components: %s', json.dumps(discovery_components, indent=4)) + log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4))) + + if existing_obj or not dry_run: + for component in TEMPLATE_COMPONENT_ORDER: + log.info('\n\n\n\n\nCOMPONENT: %s\n\n', six.text_type(json.dumps(component))) + # 1) query for components which belongs to the template + existing_c_list = _get_existing_template_c_list(component, template_id, **kwargs) + existing_c_list_subs = __salt__['zabbix.substitute_params'](existing_c_list, **kwargs) \ + if existing_c_list else [] + + if component in template_components: + defined_c_list_subs = __salt__['zabbix.substitute_params']( + template_components[component], + extend_params={TEMPLATE_COMPONENT_DEF[component]['qselectpid']: template_id}, + filter_key=TEMPLATE_COMPONENT_DEF[component]['filter'], + **kwargs) + else: + defined_c_list_subs = [] + # 2) take lists of particular component and compare -> do create, update and delete actions + _manage_component(component, template_id, defined_c_list_subs, existing_c_list_subs, **kwargs) + + log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4))) + + for d_rule_component in discovery_components: + # query for parent id -> "query_pid": {"filter_val": "vfs.fs.discovery", "component": "discoveries"} + q_def = d_rule_component['query_pid'] + c_def = TEMPLATE_COMPONENT_DEF[q_def['component']] + q_object = c_def['qtype'] + q_params = dict(c_def['output']) + q_params.update({c_def['qselectpid']: template_id}) + q_params.update({'filter': {c_def['filter']: q_def['filter_val']}}) + + parent_id = __salt__['zabbix.get_object_id_by_params'](q_object, q_params, **kwargs) + + for proto_name in DISCOVERYRULE_COMPONENT_ORDER: + log.info('\n\n\n\n\nPROTOTYPE_NAME: %s\n\n', six.text_type(json.dumps(proto_name))) + existing_p_list = _get_existing_template_c_list(proto_name, parent_id, **kwargs) + existing_p_list_subs = __salt__['zabbix.substitute_params'](existing_p_list, **kwargs)\ + if existing_p_list else [] + + if proto_name in d_rule_component: + defined_p_list_subs = __salt__['zabbix.substitute_params']( + d_rule_component[proto_name], + extend_params={c_def['qselectpid']: template_id}, + **kwargs) + else: + defined_p_list_subs = [] + + _manage_component(proto_name, + parent_id, + defined_p_list_subs, + existing_p_list_subs, + template_id=template_id, + **kwargs) + + log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4))) + + if not CHANGE_STACK: + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" already exists and corresponds to a definition.'.format(name) + else: + tmpl_action = next((item for item in CHANGE_STACK + if item['component'] == 'template' and item['action'] == 'create'), None) + if tmpl_action: + ret['result'] = True + if dry_run: + ret['comment'] = 'Zabbix Template "{0}" would be created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" does not exist.'.format(name), + 'new': 'Zabbix Template "{0}" would be created ' + 'according definition.'.format(name)}} + else: + ret['comment'] = 'Zabbix Template "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" did not exist.'.format(name), + 'new': 'Zabbix Template "{0}" created according definition.'.format(name)}} + else: + ret['result'] = True + if dry_run: + ret['comment'] = 'Zabbix Template "{0}" would be updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" differs.'.format(name), + 'new': 'Zabbix Template "{0}" would be updated ' + 'according definition.'.format(name)}} + else: + ret['comment'] = 'Zabbix Template "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" differed.'.format(name), + 'new': 'Zabbix Template "{0}" updated according definition.'.format(name)}} + + return ret + + +def absent(name, **kwargs): + ''' + Makes the Zabbix Template to be absent (either does not exist or delete it). + + :param name: Zabbix Template name + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + .. code-block:: yaml + + zabbix-template-absent: + zabbix_template.absent: + - name: Ceph OSD + ''' + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + try: + object_id = __salt__['zabbix.get_object_id_by_params']('template', {'filter': {'name': name}}, **kwargs) + except SaltException: + object_id = False + + if not object_id: + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" does not exist.'.format(name) + else: + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" exists.'.format(name), + 'new': 'Zabbix Template "{0}" would be deleted.'.format(name)}} + else: + tmpl_delete = __salt__['zabbix.run_query']('template.delete', [object_id], **kwargs) + if tmpl_delete: + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" existed.'.format(name), + 'new': 'Zabbix Template "{0}" deleted.'.format(name)}} + + return ret diff --git a/salt/states/zabbix_valuemap.py b/salt/states/zabbix_valuemap.py new file mode 100644 index 0000000000..86d2bf323a --- /dev/null +++ b/salt/states/zabbix_valuemap.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +''' +.. versionadded:: 2017.7 + +Management of Zabbix Valuemap object over Zabbix API. + +:codeauthor: Jakub Sliva +''' +from __future__ import absolute_import +from __future__ import unicode_literals +import logging +import json + +try: + from salt.ext import six + from salt.exceptions import SaltException + IMPORTS_OK = True +except ImportError: + IMPORTS_OK = False + + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only make these states available if Zabbix module and run_query function is available + and all 3rd party modules imported. + ''' + if 'zabbix.run_query' in __salt__ and IMPORTS_OK: + return True + return False, 'Import zabbix or other needed modules failed.' + + +def present(name, params, **kwargs): + ''' + Creates Zabbix Value map object or if differs update it according defined parameters + + :param name: Zabbix Value map name + :param params: Definition of the Zabbix Value map + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + .. code-block:: yaml + + zabbix-valuemap-present: + zabbix_valuemap.present: + - name: Number mapping + - params: + mappings: + - value: 1 + newvalue: one + - value: 2 + newvalue: two + ''' + zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']() + + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + # Create input params substituting functions with their results + params['name'] = name + input_params = __salt__['zabbix.substitute_params'](params, **kwargs) + log.info('Zabbix Value map: input params: %s', six.text_type(json.dumps(input_params, indent=4))) + + search = {'output': 'extend', + 'selectMappings': 'extend', + 'filter': { + 'name': name + }} + # GET Value map object if exists + valuemap_get = __salt__['zabbix.run_query']('valuemap.get', search, **kwargs) + log.info('Zabbix Value map: valuemap.get result: %s', six.text_type(json.dumps(valuemap_get, indent=4))) + + existing_obj = __salt__['zabbix.substitute_params'](valuemap_get[0], **kwargs) \ + if valuemap_get and len(valuemap_get) == 1 else False + + if existing_obj: + diff_params = __salt__['zabbix.compare_params'](input_params, existing_obj) + log.info('Zabbix Value map: input params: {%s', six.text_type(json.dumps(input_params, indent=4))) + log.info('Zabbix Value map: Object comparison result. Differences: %s', six.text_type(diff_params)) + + if diff_params: + diff_params[zabbix_id_mapper['valuemap']] = existing_obj[zabbix_id_mapper['valuemap']] + log.info('Zabbix Value map: update params: %s', six.text_type(json.dumps(diff_params, indent=4))) + + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" would be fixed.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" differs ' + 'in following parameters: {1}'.format(name, diff_params), + 'new': 'Zabbix Value map "{0}" would correspond to definition.'.format(name)}} + else: + valuemap_update = __salt__['zabbix.run_query']('valuemap.update', diff_params, **kwargs) + log.info('Zabbix Value map: valuemap.update result: %s', six.text_type(valuemap_update)) + if valuemap_update: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" differed ' + 'in following parameters: {1}'.format(name, diff_params), + 'new': 'Zabbix Value map "{0}" fixed.'.format(name)}} + + else: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" already exists and corresponds to a definition.'.format(name) + + else: + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" would be created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" does not exist.'.format(name), + 'new': 'Zabbix Value map "{0}" would be created ' + 'according definition.'.format(name)}} + else: + # ACTION.CREATE + valuemap_create = __salt__['zabbix.run_query']('valuemap.create', input_params, **kwargs) + log.info('Zabbix Value map: valuemap.create result: ' + six.text_type(valuemap_create)) + + if valuemap_create: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" did not exist.'.format(name), + 'new': 'Zabbix Value map "{0}" created according definition.'.format(name)}} + + return ret + + +def absent(name, **kwargs): + ''' + Makes the Zabbix Value map to be absent (either does not exist or delete it). + + :param name: Zabbix Value map name + :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring) + :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring) + :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring) + + .. code-block:: yaml + + zabbix-valuemap-absent: + zabbix_valuemap.absent: + - name: Value map name + ''' + dry_run = __opts__['test'] + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + try: + object_id = __salt__['zabbix.get_object_id_by_params']('valuemap', {'filter': {'name': name}}, **kwargs) + except SaltException: + object_id = False + + if not object_id: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" does not exist.'.format(name) + else: + if dry_run: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" exists.'.format(name), + 'new': 'Zabbix Value map "{0}" would be deleted.'.format(name)}} + else: + valuemap_delete = __salt__['zabbix.run_query']('valuemap.delete', [object_id], **kwargs) + + if valuemap_delete: + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" existed.'.format(name), + 'new': 'Zabbix Value map "{0}" deleted.'.format(name)}} + + return ret diff --git a/tests/unit/modules/test_zabbix.py b/tests/unit/modules/test_zabbix.py index d8148d4ed4..1d538736d9 100644 --- a/tests/unit/modules/test_zabbix.py +++ b/tests/unit/modules/test_zabbix.py @@ -3,18 +3,97 @@ :codeauthor: :email:`Christian McHugh ` ''' -# Import python libs +# Import Python Libs from __future__ import absolute_import +from __future__ import unicode_literals import salt.modules.zabbix as zabbix -# Import Salt Testing libs +# Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase -from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +from salt.exceptions import SaltException CONN_ARGS = {} CONN_ARGS['url'] = 'http://test.url' CONN_ARGS['auth'] = '1234' + +GETID_QUERY_RESULT_OK = [{'internal': '0', 'flags': '0', 'groupid': '11', 'name': 'Databases'}] +GETID_QUERY_RESULT_BAD = [{'internal': '0', 'flags': '0', 'groupid': '11', 'name': 'Databases'}, {'another': 'object'}] + +DEFINED_PARAMS = {'name': 'beta', + 'eventsource': 2, + 'status': 0, + 'filter': {'evaltype': 2, + 'conditions': [{'conditiontype': 24, + 'operator': 2, + 'value': 'db'}]}, + 'operations': [{'operationtype': 2}, + {'operationtype': 4, + 'opgroup': [{'groupid': {'query_object': 'hostgroup', + 'query_name': 'Databases'}}]}], + 'empty_list': []} + +SUBSTITUTED_DEFINED_PARAMS = {'status': '0', + 'filter': {'evaltype': '2', + 'conditions': [{'operator': '2', + 'conditiontype': '24', + 'value': 'db'}]}, + 'eventsource': '2', + 'name': 'beta', + 'operations': [{'operationtype': '2'}, + {'opgroup': [{'groupid': '11'}], + 'operationtype': '4'}], + 'empty_list': []} + +EXISTING_OBJECT_PARAMS = {'status': '0', + 'operations': [{'operationtype': '2', 'esc_period': '0', 'evaltype': '0', 'opconditions': [], + 'esc_step_to': '1', 'actionid': '23', 'esc_step_from': '1', + 'operationid': '64'}, + {'operationtype': '4', 'esc_period': '0', 'evaltype': '0', 'opconditions': [], + 'esc_step_to': '1', 'actionid': '23', 'esc_step_from': '1', + 'opgroup': [{'groupid': '11', + 'operationid': '65'}], + 'operationid': '65'}], + 'def_shortdata': '', + 'name': 'beta', + 'esc_period': '0', + 'def_longdata': '', + 'filter': {'formula': '', + 'evaltype': '2', + 'conditions': [{'operator': '2', + 'conditiontype': '24', + 'formulaid': 'A', + 'value': 'DIFFERENT VALUE HERE'}], + 'eval_formula': 'A'}, + 'eventsource': '2', + 'actionid': '23', + 'r_shortdata': '', + 'r_longdata': '', + 'recovery_msg': '0', + 'empty_list': [{'dict_key': 'dic_val'}]} + +DIFF_PARAMS_RESULT = {'filter': {'evaltype': '2', + 'conditions': [{'operator': '2', + 'conditiontype': '24', + 'value': 'db'}]}, + 'empty_list': []} + +DIFF_PARAMS_RESULT_WITH_ROLLBACK = {'new': DIFF_PARAMS_RESULT, + 'old': {'filter': {'formula': '', + 'evaltype': '2', + 'conditions': [{'operator': '2', + 'conditiontype': '24', + 'formulaid': 'A', + 'value': 'DIFFERENT VALUE HERE'}], + 'eval_formula': 'A'}, + 'empty_list': [{'dict_key': 'dic_val'}]}} @skipIf(NO_MOCK, NO_MOCK_REASON) @@ -26,6 +105,53 @@ class ZabbixTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {zabbix: {'__salt__': {'cmd.which_bin': lambda _: 'zabbix_server'}}} + def test_get_object_id_by_params(self): + ''' + Test get_object_id function with expected result from API call + ''' + with patch('salt.modules.zabbix.run_query', MagicMock(return_value=GETID_QUERY_RESULT_OK)): + self.assertEqual(zabbix.get_object_id_by_params('hostgroup', 'Databases'), '11') + + def test_get_obj_id_by_params_fail(self): + ''' + Test get_object_id function with unexpected result from API call + ''' + with patch('salt.modules.zabbix.run_query', MagicMock(return_value=GETID_QUERY_RESULT_BAD)): + self.assertRaises(SaltException, zabbix.get_object_id_by_params, 'hostgroup', 'Databases') + + def test_substitute_params(self): + ''' + Test proper parameter substitution for defined input + ''' + with patch('salt.modules.zabbix.get_object_id_by_params', MagicMock(return_value='11')): + self.assertEqual(zabbix.substitute_params(DEFINED_PARAMS), SUBSTITUTED_DEFINED_PARAMS) + + def test_substitute_params_fail(self): + ''' + Test proper parameter substitution if there is needed parameter missing + ''' + self.assertRaises(SaltException, zabbix.substitute_params, {'groupid': {'query_object': 'hostgroup'}}) + + def test_compare_params(self): + ''' + Test result comparison of two params structures + ''' + self.assertEqual(zabbix.compare_params(SUBSTITUTED_DEFINED_PARAMS, EXISTING_OBJECT_PARAMS), + DIFF_PARAMS_RESULT) + + def test_compare_params_rollback(self): + ''' + Test result comparison of two params structures with rollback return value option + ''' + self.assertEqual(zabbix.compare_params(SUBSTITUTED_DEFINED_PARAMS, EXISTING_OBJECT_PARAMS, True), + DIFF_PARAMS_RESULT_WITH_ROLLBACK) + + def test_compare_params_fail(self): + ''' + Test result comparison of two params structures where some data type mismatch exists + ''' + self.assertRaises(SaltException, zabbix.compare_params, {'dict': 'val'}, {'dict': ['list']}) + def test_apiiinfo_version(self): ''' Test apiinfo_version diff --git a/tests/unit/states/test_zabbix_action.py b/tests/unit/states/test_zabbix_action.py new file mode 100644 index 0000000000..757b6ba588 --- /dev/null +++ b/tests/unit/states/test_zabbix_action.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jakub Sliva ` +''' + +# Import Python Libs +from __future__ import absolute_import +from __future__ import unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +import salt.states.zabbix_action as zabbix_action + + +INPUT_PARAMS = { + 'status': '0', + 'filter': {'evaltype': '2', 'conditions': [{'operator': '2', 'conditiontype': '24', 'value': 'database'}]}, + 'eventsource': '2', + 'name': 'Auto registration Databases', + 'operations': [{'opgroup': [{'groupid': '6'}], 'operationtype': '4'}]} + +EXISTING_OBJ = [{ + 'status': '0', + 'operations': [{'operationtype': '4', 'esc_period': '0', 'evaltype': '0', 'opconditions': [], + 'esc_step_to': '1', 'actionid': '28', 'esc_step_from': '1', + 'opgroup': [{'groupid': '6', 'operationid': '92'}], + 'operationid': '92'}], + 'def_shortdata': '', + 'name': 'Auto registration Databases', + 'esc_period': '0', + 'def_longdata': '', + 'filter': {'formula': '', 'evaltype': '2', 'conditions': [{'operator': '2', 'conditiontype': '24', + 'formulaid': 'A', 'value': 'database'}], + 'eval_formula': 'A'}, + 'eventsource': '2', + 'actionid': '28', + 'r_shortdata': '', + 'r_longdata': '', + 'recovery_msg': '0'}] + +EXISTING_OBJ_DIFF = { + 'status': '0', + 'operations': [{'operationtype': '4', 'esc_period': '0', 'evaltype': '0', 'opconditions': [], + 'esc_step_to': '1', 'actionid': '28', 'esc_step_from': '1', + 'opgroup': [{'groupid': '6', 'operationid': '92'}], + 'operationid': '92'}], + 'def_shortdata': '', + 'name': 'Auto registration Databases', + 'esc_period': '0', + 'def_longdata': '', + 'filter': {'formula': '', 'evaltype': '2', 'conditions': [{'operator': '2', 'conditiontype': '24', + 'formulaid': 'A', 'value': 'SOME OTHER VALUE'}], + 'eval_formula': 'A'}, + 'eventsource': '2', + 'actionid': '28', + 'r_shortdata': '', + 'r_longdata': '', + 'recovery_msg': '0'} + +DIFF_PARAMS = {'filter': {'evaltype': '2', + 'conditions': [{'operator': '2', 'conditiontype': '24', 'value': 'virtual'}]}, + 'actionid': '28'} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ZabbixActionTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.zabbix + ''' + def setup_loader_modules(self): + return {zabbix_action: {}} + + def test_present_create(self): + ''' + Test to ensure that named action is created + ''' + name = 'Auto registration Databases' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'action.get': + return False + elif args[0] == 'action.create': + return True + + with patch.dict(zabbix_action.__opts__, {'test': False}): + with patch.dict(zabbix_action.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'action': 'actionid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, False]), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value={})}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" did not exist.'.format(name), + 'new': 'Zabbix Action "{0}" created according definition.'.format(name)}} + self.assertDictEqual(zabbix_action.present(name, {}), ret) + + def test_present_exists(self): + ''' + Test to ensure that named action is present and not changed + ''' + name = 'Auto registration Databases' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + with patch.dict(zabbix_action.__opts__, {'test': False}): + with patch.dict(zabbix_action.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'action': 'actionid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, EXISTING_OBJ]), + 'zabbix.run_query': MagicMock(return_value=['length of result is 1']), + 'zabbix.compare_params': MagicMock(return_value={})}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" already exists and corresponds to a definition.'.format(name) + self.assertDictEqual(zabbix_action.present(name, {}), ret) + + def test_present_update(self): + ''' + Test to ensure that named action is present but must be updated + ''' + name = 'Auto registration Databases' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'action.get': + return ['length of result is 1 = action exists'] + elif args[0] == 'action.update': + return DIFF_PARAMS + + with patch.dict(zabbix_action.__opts__, {'test': False}): + with patch.dict(zabbix_action.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'action': 'actionid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, EXISTING_OBJ_DIFF]), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value=DIFF_PARAMS)}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" differed ' + 'in following parameters: {1}'.format(name, DIFF_PARAMS), + 'new': 'Zabbix Action "{0}" fixed.'.format(name)}} + self.assertDictEqual(zabbix_action.present(name, {}), ret) + + def test_absent_test_mode(self): + ''' + Test to ensure that named action is absent in test mode + ''' + name = 'Auto registration Databases' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_action.__opts__, {'test': True}): + with patch.dict(zabbix_action.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" exists.'.format(name), + 'new': 'Zabbix Action "{0}" would be deleted.'.format(name)}} + self.assertDictEqual(zabbix_action.absent(name), ret) + + def test_absent(self): + ''' + Test to ensure that named action is absent + ''' + name = 'Auto registration Databases' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_action.__opts__, {'test': False}): + with patch.dict(zabbix_action.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=False)}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" does not exist.'.format(name) + self.assertDictEqual(zabbix_action.absent(name), ret) + + with patch.dict(zabbix_action.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + with patch.dict(zabbix_action.__salt__, {'zabbix.run_query': MagicMock(return_value=True)}): + ret['result'] = True + ret['comment'] = 'Zabbix Action "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Action "{0}" existed.'.format(name), + 'new': 'Zabbix Action "{0}" deleted.'.format(name)}} + self.assertDictEqual(zabbix_action.absent(name), ret) diff --git a/tests/unit/states/test_zabbix_template.py b/tests/unit/states/test_zabbix_template.py new file mode 100644 index 0000000000..27112db52e --- /dev/null +++ b/tests/unit/states/test_zabbix_template.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jakub Sliva ` +''' + +# Import Python Libs +from __future__ import absolute_import +from __future__ import unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +import salt.states.zabbix_template as zabbix_template + + +INPUT_PARAMS = {"applications": [{"name": "Ceph OSD"}]} + +DEFINED_OBJ = {"macros": [{"macro": "{$CEPH_CLUSTER_NAME}", "value": "ceph"}], "host": "A Testing Template", + "hosts": [{"hostid": "10112"}, {"hostid": "10113"}], "description": "Template for Ceph nodes", + "groups": [{"groupid": "1"}]} + +DEFINED_C_LIST_SUBS = {"applications": [{"name": "Ceph OSD"}], 'graphs': [], 'triggers': [], 'items': [], + 'httpTests': [], 'screens': [], 'gitems': [], 'discoveries': []} + +SUBSTITUTE_PARAMS_CREATE = [DEFINED_OBJ, [], DEFINED_C_LIST_SUBS['applications'], [], [], [], [], [], [], [], []] + +EXISTING_OBJ = [{"available": "0", "tls_connect": "1", "maintenance_type": "0", "groups": [{"groupid": "1"}], + "macros": [{"macro": "{$CEPH_CLUSTER_NAME}", "hostmacroid": "60", "hostid": "10206", "value": "ceph"}], + "hosts": [{"hostid": "10112"}, {"hostid": "10113"}], "status": "3", + "description": "Template for Ceph nodes", "host": "A Testing Template", "disable_until": "0", + "templateid": "10206", "name": "A Testing Template"}] + +SUBSTITUTE_PARAMS_EXISTS = [DEFINED_OBJ, EXISTING_OBJ[0], [], [], [], [], [], [], [], []] + +EXISTING_OBJ_DIFF = [{"groups": [{"groupid": "1"}], "macros": [{"macro": "{$CEPH_CLUSTER_NAME}", "hostmacroid": "60", + "hostid": "10206", "value": "ceph"}], + "hosts": [{"hostid": "10112"}, {"hostid": "10113"}], "status": "3", "templateid": "10206", + "name": "A Testing Template"}] + +SUBSTITUTE_PARAMS_UPDATE = [DEFINED_OBJ, EXISTING_OBJ_DIFF[0], [], [], [], [], [], [], [], []] + +DIFF_PARAMS = {'old': {}, 'new': {'macros': [], 'templateid': '10206'}} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ZabbixTemplateTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.zabbix + ''' + def setup_loader_modules(self): + return {zabbix_template: {}} + + @patch('salt.states.zabbix_template.CHANGE_STACK', []) + def test_present_create(self): + ''' + Test to ensure that named template is created + ''' + name = 'A Testing Template' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'template.get': + return [] + elif args[0] == 'template.create': + return {'templateids': ['10206']} + elif args[0] == 'application.get': + return [] + elif args[0] == 'application.create': + return {"applicationids": ["701"]} + + with patch.dict(zabbix_template.__opts__, {'test': False}): + with patch.dict(zabbix_template.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'template': 'templateid'}), + 'zabbix.substitute_params': MagicMock(side_effect=SUBSTITUTE_PARAMS_CREATE), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value={})}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" did not exist.'.format(name), + 'new': 'Zabbix Template "{0}" created according definition.'.format(name)}} + self.assertDictEqual(zabbix_template.present(name, {}), ret) + + @patch('salt.states.zabbix_template.CHANGE_STACK', []) + def test_present_exists(self): + ''' + Test to ensure that named template is present and not changed + ''' + name = 'A Testing Template' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'template.get': + return EXISTING_OBJ + elif args[0] == 'application.get': + return ['non-empty'] + + with patch.dict(zabbix_template.__opts__, {'test': False}): + with patch.dict(zabbix_template.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'template': 'templateid'}), + 'zabbix.substitute_params': MagicMock(side_effect=SUBSTITUTE_PARAMS_EXISTS), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value={'new': {}, 'old': {}})}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" already exists and corresponds to a definition.'.format(name) + self.assertDictEqual(zabbix_template.present(name, {}), ret) + + @patch('salt.states.zabbix_template.CHANGE_STACK', []) + def test_present_update(self): + ''' + Test to ensure that named template is present but must be updated + ''' + name = 'A Testing Template' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'template.get': + return ['length of result is 1 = template exists'] + elif args[0] == 'template.update': + return DIFF_PARAMS + + with patch.dict(zabbix_template.__opts__, {'test': False}): + with patch.dict(zabbix_template.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'template': 'templateid'}), + 'zabbix.substitute_params': MagicMock(side_effect=SUBSTITUTE_PARAMS_UPDATE), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value=DIFF_PARAMS)}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" differed.'.format(name), + 'new': 'Zabbix Template "{0}" updated according definition.'.format(name)}} + self.assertDictEqual(zabbix_template.present(name, {}), ret) + + def test_absent_test_mode(self): + ''' + Test to ensure that named template is absent in test mode + ''' + name = 'A Testing Template' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_template.__opts__, {'test': True}): + with patch.dict(zabbix_template.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" exists.'.format(name), + 'new': 'Zabbix Template "{0}" would be deleted.'.format(name)}} + self.assertDictEqual(zabbix_template.absent(name), ret) + + def test_absent(self): + ''' + Test to ensure that named template is absent + ''' + name = 'A Testing Template' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_template.__opts__, {'test': False}): + with patch.dict(zabbix_template.__salt__, + {'zabbix.get_object_id_by_params': MagicMock(return_value=False)}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" does not exist.'.format(name) + self.assertDictEqual(zabbix_template.absent(name), ret) + + with patch.dict(zabbix_template.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + with patch.dict(zabbix_template.__salt__, {'zabbix.run_query': MagicMock(return_value=True)}): + ret['result'] = True + ret['comment'] = 'Zabbix Template "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Template "{0}" existed.'.format(name), + 'new': 'Zabbix Template "{0}" deleted.'.format(name)}} + self.assertDictEqual(zabbix_template.absent(name), ret) diff --git a/tests/unit/states/test_zabbix_valuemap.py b/tests/unit/states/test_zabbix_valuemap.py new file mode 100644 index 0000000000..fe0d7207f0 --- /dev/null +++ b/tests/unit/states/test_zabbix_valuemap.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jakub Sliva ` +''' + +# Import Python Libs +from __future__ import absolute_import +from __future__ import unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +import salt.states.zabbix_valuemap as zabbix_valuemap + + +INPUT_PARAMS = {'mappings': [{'newvalue': 'OK', 'value': '0h'}, {'newvalue': 'Failure', 'value': '1'}], + 'name': 'Server HP Health'} + +EXISTING_OBJ = [{'valuemapid': '21', 'name': 'Server HP Health', 'mappings': [{'newvalue': 'OK', 'value': '0h'}, + {'newvalue': 'Failure', 'value': '1'}]}] + +EXISTING_OBJ_DIFF = {'valuemapid': '21', 'name': 'Server HP Health', 'mappings': [{'newvalue': 'OK', 'value': '0h'}, + {'newvalue': 'Failure', 'value': '1'}, + {'newvalue': 'some', 'value': '2'}]} + +DIFF_PARAMS = {'valuemapid': '21', 'mappings': [{'newvalue': 'OK', 'value': '0h'}, + {'newvalue': 'Failure', 'value': '1'}]} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ZabbixActionTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.zabbix + ''' + def setup_loader_modules(self): + return {zabbix_valuemap: {}} + + def test_present_create(self): + ''' + Test to ensure that named value map is created + ''' + name = 'Server HP Health' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'valuemap.get': + return False + elif args[0] == 'valuemap.create': + return True + + with patch.dict(zabbix_valuemap.__opts__, {'test': False}): + with patch.dict(zabbix_valuemap.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'valuemap': 'valuemapid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, False]), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value={})}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" created.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" did not exist.'.format(name), + 'new': 'Zabbix Value map "{0}" created according definition.'.format(name)}} + self.assertDictEqual(zabbix_valuemap.present(name, {}), ret) + + def test_present_exists(self): + ''' + Test to ensure that named value map is present and not changed + ''' + name = 'Server HP Health' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + with patch.dict(zabbix_valuemap.__opts__, {'test': False}): + with patch.dict(zabbix_valuemap.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'valuemap': 'valuemapid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, EXISTING_OBJ]), + 'zabbix.run_query': MagicMock(return_value=['length of result is 1']), + 'zabbix.compare_params': MagicMock(return_value={})}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" already exists and corresponds to a definition.'.format(name) + self.assertDictEqual(zabbix_valuemap.present(name, {}), ret) + + def test_present_update(self): + ''' + Test to ensure that named value map is present but must be updated + ''' + name = 'Server HP Health' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + + def side_effect_run_query(*args): + ''' + Differentiate between __salt__ exec module function calls with different parameters. + ''' + if args[0] == 'valuemap.get': + return ['length of result is 1 = valuemap exists'] + elif args[0] == 'valuemap.update': + return DIFF_PARAMS + + with patch.dict(zabbix_valuemap.__opts__, {'test': False}): + with patch.dict(zabbix_valuemap.__salt__, + {'zabbix.get_zabbix_id_mapper': MagicMock(return_value={'valuemap': 'valuemapid'}), + 'zabbix.substitute_params': MagicMock(side_effect=[INPUT_PARAMS, EXISTING_OBJ_DIFF]), + 'zabbix.run_query': MagicMock(side_effect=side_effect_run_query), + 'zabbix.compare_params': MagicMock(return_value=DIFF_PARAMS)}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" updated.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" differed ' + 'in following parameters: {1}'.format(name, DIFF_PARAMS), + 'new': 'Zabbix Value map "{0}" fixed.'.format(name)}} + self.assertDictEqual(zabbix_valuemap.present(name, {}), ret) + + def test_absent_test_mode(self): + ''' + Test to ensure that named value map is absent in test mode + ''' + name = 'Server HP Health' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_valuemap.__opts__, {'test': True}): + with patch.dict(zabbix_valuemap.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" would be deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" exists.'.format(name), + 'new': 'Zabbix Value map "{0}" would be deleted.'.format(name)}} + self.assertDictEqual(zabbix_valuemap.absent(name), ret) + + def test_absent(self): + ''' + Test to ensure that named value map is absent + ''' + name = 'Server HP Health' + ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} + with patch.dict(zabbix_valuemap.__opts__, {'test': False}): + with patch.dict(zabbix_valuemap.__salt__, + {'zabbix.get_object_id_by_params': MagicMock(return_value=False)}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" does not exist.'.format(name) + self.assertDictEqual(zabbix_valuemap.absent(name), ret) + + with patch.dict(zabbix_valuemap.__salt__, {'zabbix.get_object_id_by_params': MagicMock(return_value=11)}): + with patch.dict(zabbix_valuemap.__salt__, {'zabbix.run_query': MagicMock(return_value=True)}): + ret['result'] = True + ret['comment'] = 'Zabbix Value map "{0}" deleted.'.format(name) + ret['changes'] = {name: {'old': 'Zabbix Value map "{0}" existed.'.format(name), + 'new': 'Zabbix Value map "{0}" deleted.'.format(name)}} + self.assertDictEqual(zabbix_valuemap.absent(name), ret)