saltstack/salt

View on GitHub
salt/states/zabbix_host.py

Summary

Maintainability
F
1 wk
Test Coverage
# -*- coding: utf-8 -*-
'''
Management of Zabbix hosts.

:codeauthor: Jiri Kotlin <jiri.kotlin@ultimum.io>


'''
from __future__ import absolute_import, print_function, unicode_literals
from salt.utils.json import loads, dumps
from copy import deepcopy
from salt.ext import six


def __virtual__():
    '''
    Only make these states available if Zabbix module is available.
    '''
    return 'zabbix.host_create' in __salt__


def present(host, groups, interfaces, **kwargs):
    '''
    Ensures that the host exists, eventually creates new host.
    NOTE: please use argument visible_name instead of name to not mess with name from salt sls. This function accepts
    all standard host properties: keyword argument names differ depending on your zabbix version, see:
    https://www.zabbix.com/documentation/2.4/manual/api/reference/host/object#host

    .. versionadded:: 2016.3.0

    :param host: technical name of the host
    :param groups: groupids of host groups to add the host to
    :param interfaces: interfaces to be created for the host
    :param proxy_host: Optional proxy name or proxyid to monitor host
    :param inventory: Optional list of inventory names and values
    :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)
    :param visible_name: Optional - string with visible name of the host, use 'visible_name' instead of 'name' \
    parameter to not mess with value supplied from Salt sls file.

    .. code-block:: yaml

        create_test_host:
            zabbix_host.present:
                - host: TestHostWithInterfaces
                - proxy_host: 12345
                - groups:
                    - 5
                    - 6
                    - 7
                - interfaces:
                    - test1.example.com:
                        - ip: '192.168.1.8'
                        - type: 'Agent'
                        - port: 92
                    - testing2_create:
                        - ip: '192.168.1.9'
                        - dns: 'test2.example.com'
                        - type: 'agent'
                        - main: false
                    - testovaci1_ipmi:
                        - ip: '192.168.100.111'
                        - type: 'ipmi'
                - inventory:
                    - alias: some alias
                    - asset_tag: jlm3937


    '''
    connection_args = {}
    if '_connection_user' in kwargs:
        connection_args['_connection_user'] = kwargs['_connection_user']
    if '_connection_password' in kwargs:
        connection_args['_connection_password'] = kwargs['_connection_password']
    if '_connection_url' in kwargs:
        connection_args['_connection_url'] = kwargs['_connection_url']

    ret = {'name': host, 'changes': {}, 'result': False, 'comment': ''}

    # Comment and change messages
    comment_host_created = 'Host {0} created.'.format(host)
    comment_host_updated = 'Host {0} updated.'.format(host)
    comment_host_notcreated = 'Unable to create host: {0}. '.format(host)
    comment_host_exists = 'Host {0} already exists.'.format(host)
    changes_host_created = {host: {'old': 'Host {0} does not exist.'.format(host),
                                   'new': 'Host {0} created.'.format(host),
                                   }
                            }

    def _interface_format(interfaces_data):
        '''
        Formats interfaces from SLS file into valid JSON usable for zabbix API.
        Completes JSON with default values.

        :param interfaces_data: list of interfaces data from SLS file

        '''

        if not interfaces_data:
            return list()

        interface_attrs = ('ip', 'dns', 'main', 'type', 'useip', 'port')
        interfaces_json = loads(dumps(interfaces_data))
        interfaces_dict = dict()

        for interface in interfaces_json:
            for intf in interface:
                intf_name = intf
                interfaces_dict[intf_name] = dict()
                for intf_val in interface[intf]:
                    for key, value in intf_val.items():
                        if key in interface_attrs:
                            interfaces_dict[intf_name][key] = value

        interfaces_list = list()
        interface_ports = {'agent': ['1', '10050'], 'snmp': ['2', '161'], 'ipmi': ['3', '623'],
                           'jmx': ['4', '12345']}

        for key, value in interfaces_dict.items():
            # Load interface values or default values
            interface_type = interface_ports[value['type'].lower()][0]
            main = '1' if six.text_type(value.get('main', 'true')).lower() == 'true' else '0'
            useip = '1' if six.text_type(value.get('useip', 'true')).lower() == 'true' else '0'
            interface_ip = value.get('ip', '')
            dns = value.get('dns', key)
            port = six.text_type(value.get('port', interface_ports[value['type'].lower()][1]))

            interfaces_list.append({'type': interface_type,
                                    'main': main,
                                    'useip': useip,
                                    'ip': interface_ip,
                                    'dns': dns,
                                    'port': port})

        interfaces_list = interfaces_list
        interfaces_list_sorted = sorted(interfaces_list, key=lambda k: k['main'], reverse=True)

        return interfaces_list_sorted

    interfaces_formated = _interface_format(interfaces)

    # Ensure groups are all groupid
    groupids = []
    for group in groups:
        if isinstance(group, six.string_types):
            groupid = __salt__['zabbix.hostgroup_get'](name=group, **connection_args)
            try:
                groupids.append(int(groupid[0]['groupid']))
            except TypeError:
                ret['comment'] = 'Invalid group {0}'.format(group)
                return ret
        else:
            groupids.append(group)
    groups = groupids

    # Get and validate proxyid
    proxy_hostid = "0"
    if 'proxy_host' in kwargs:
        # Test if proxy_host given as name
        if isinstance(kwargs['proxy_host'], six.string_types):
            try:
                proxy_hostid = __salt__['zabbix.run_query']('proxy.get', {"output": "proxyid",
                                                            "selectInterface": "extend",
                                                            "filter": {"host": "{0}".format(kwargs['proxy_host'])}},
                                                            **connection_args)[0]['proxyid']
            except TypeError:
                ret['comment'] = 'Invalid proxy_host {0}'.format(kwargs['proxy_host'])
                return ret
        # Otherwise lookup proxy_host as proxyid
        else:
            try:
                proxy_hostid = __salt__['zabbix.run_query']('proxy.get', {"proxyids":
                                                            "{0}".format(kwargs['proxy_host']),
                                                            "output": "proxyid"},
                                                            **connection_args)[0]['proxyid']
            except TypeError:
                ret['comment'] = 'Invalid proxy_host {0}'.format(kwargs['proxy_host'])
                return ret

    if 'inventory' not in kwargs:
        inventory = {}
    else:
        inventory = kwargs['inventory']
    if inventory is None:
        inventory = {}
    # Create dict of requested inventory items
    new_inventory = {}
    for inv_item in inventory:
        for k, v in inv_item.items():
            new_inventory[k] = str(v)

    host_exists = __salt__['zabbix.host_exists'](host, **connection_args)

    if host_exists:
        host = __salt__['zabbix.host_get'](host=host, **connection_args)[0]
        hostid = host['hostid']

        update_proxy = False
        update_hostgroups = False
        update_interfaces = False
        update_inventory = False

        cur_proxy_hostid = host['proxy_hostid']
        if proxy_hostid != cur_proxy_hostid:
            update_proxy = True

        hostgroups = __salt__['zabbix.hostgroup_get'](hostids=hostid, **connection_args)
        cur_hostgroups = list()

        for hostgroup in hostgroups:
            cur_hostgroups.append(int(hostgroup['groupid']))

        if set(groups) != set(cur_hostgroups):
            update_hostgroups = True

        hostinterfaces = __salt__['zabbix.hostinterface_get'](hostids=hostid, **connection_args)

        if hostinterfaces:
            hostinterfaces = sorted(hostinterfaces, key=lambda k: k['main'])
            hostinterfaces_copy = deepcopy(hostinterfaces)
            for hostintf in hostinterfaces_copy:
                hostintf.pop('interfaceid')
                hostintf.pop('bulk')
                hostintf.pop('hostid')
            interface_diff = [x for x in interfaces_formated if x not in hostinterfaces_copy] + \
                             [y for y in hostinterfaces_copy if y not in interfaces_formated]
            if interface_diff:
                update_interfaces = True

        elif not hostinterfaces and interfaces:
            update_interfaces = True

        cur_inventory = __salt__['zabbix.host_inventory_get'](hostids=hostid, **connection_args)
        if cur_inventory:
            # Remove blank inventory items
            cur_inventory = {k: v for k, v in cur_inventory.items() if v}
            # Remove persistent inventory keys for comparison
            cur_inventory.pop('hostid', None)
            cur_inventory.pop('inventory_mode', None)

        if new_inventory and not cur_inventory:
            update_inventory = True
        elif set(cur_inventory) != set(new_inventory):
            update_inventory = True

    # Dry run, test=true mode
    if __opts__['test']:
        if host_exists:
            if update_hostgroups or update_interfaces or update_proxy or update_inventory:
                ret['result'] = None
                ret['comment'] = comment_host_updated
            else:
                ret['result'] = True
                ret['comment'] = comment_host_exists
        else:
            ret['result'] = None
            ret['comment'] = comment_host_created
            ret['changes'] = changes_host_created
        return ret

    error = []

    if host_exists:
        ret['result'] = True
        if update_hostgroups or update_interfaces or update_proxy or update_inventory:

            if update_inventory:
                # combine connection_args, inventory, and clear_old
                sum_kwargs = dict(new_inventory)
                sum_kwargs.update(connection_args)
                sum_kwargs['clear_old'] = True

                hostupdate = __salt__['zabbix.host_inventory_set'](hostid, **sum_kwargs)
                ret['changes']['inventory'] = str(new_inventory)
                if 'error' in hostupdate:
                    error.append(hostupdate['error'])
            if update_proxy:
                hostupdate = __salt__['zabbix.host_update'](hostid, proxy_hostid=proxy_hostid, **connection_args)
                ret['changes']['proxy_hostid'] = six.text_type(proxy_hostid)
                if 'error' in hostupdate:
                    error.append(hostupdate['error'])
            if update_hostgroups:
                hostupdate = __salt__['zabbix.host_update'](hostid, groups=groups, **connection_args)
                ret['changes']['groups'] = six.text_type(groups)
                if 'error' in hostupdate:
                    error.append(hostupdate['error'])
            if update_interfaces:
                if hostinterfaces:
                    for interface in hostinterfaces:
                        __salt__['zabbix.hostinterface_delete'](interfaceids=interface['interfaceid'],
                                                                **connection_args)

                hostid = __salt__['zabbix.host_get'](name=host, **connection_args)[0]['hostid']

                for interface in interfaces_formated:
                    updatedint = __salt__['zabbix.hostinterface_create'](hostid=hostid,
                                                                         ip=interface['ip'],
                                                                         dns=interface['dns'],
                                                                         main=interface['main'],
                                                                         type=interface['type'],
                                                                         useip=interface['useip'],
                                                                         port=interface['port'],
                                                                         **connection_args)

                    if 'error' in updatedint:
                        error.append(updatedint['error'])

                ret['changes']['interfaces'] = six.text_type(interfaces_formated)

            ret['comment'] = comment_host_updated

        else:
            ret['comment'] = comment_host_exists
    else:
        host_create = __salt__['zabbix.host_create'](host,
                                                     groups,
                                                     interfaces_formated,
                                                     proxy_hostid=proxy_hostid,
                                                     inventory=new_inventory,
                                                     **connection_args)

        if 'error' not in host_create:
            ret['result'] = True
            ret['comment'] = comment_host_created
            ret['changes'] = changes_host_created
        else:
            ret['result'] = False
            ret['comment'] = comment_host_notcreated + six.text_type(host_create['error'])

    # error detected
    if error:
        ret['changes'] = {}
        ret['result'] = False
        ret['comment'] = six.text_type(error)

    return ret


def absent(name, **kwargs):
    """
    Ensures that the host does not exists, eventually deletes host.

    .. versionadded:: 2016.3.0

    :param: name: technical name of the host
    :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

        TestHostWithInterfaces:
            zabbix_host.absent

    """
    ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}

    # Comment and change messages
    comment_host_deleted = 'Host {0} deleted.'.format(name)
    comment_host_notdeleted = 'Unable to delete host: {0}. '.format(name)
    comment_host_notexists = 'Host {0} does not exist.'.format(name)
    changes_host_deleted = {name: {'old': 'Host {0} exists.'.format(name),
                                   'new': 'Host {0} deleted.'.format(name),
                                   }
                            }
    connection_args = {}
    if '_connection_user' in kwargs:
        connection_args['_connection_user'] = kwargs['_connection_user']
    if '_connection_password' in kwargs:
        connection_args['_connection_password'] = kwargs['_connection_password']
    if '_connection_url' in kwargs:
        connection_args['_connection_url'] = kwargs['_connection_url']

    host_exists = __salt__['zabbix.host_exists'](name, **connection_args)

    # Dry run, test=true mode
    if __opts__['test']:
        if not host_exists:
            ret['result'] = True
            ret['comment'] = comment_host_notexists
        else:
            ret['result'] = None
            ret['comment'] = comment_host_deleted
        return ret

    host_get = __salt__['zabbix.host_get'](name, **connection_args)

    if not host_get:
        ret['result'] = True
        ret['comment'] = comment_host_notexists
    else:
        try:
            hostid = host_get[0]['hostid']
            host_delete = __salt__['zabbix.host_delete'](hostid, **connection_args)
        except KeyError:
            host_delete = False

        if host_delete and 'error' not in host_delete:
            ret['result'] = True
            ret['comment'] = comment_host_deleted
            ret['changes'] = changes_host_deleted
        else:
            ret['result'] = False
            ret['comment'] = comment_host_notdeleted + six.text_type(host_delete['error'])

    return ret


def assign_templates(host, templates, **kwargs):
    '''
    Ensures that templates are assigned to the host.

    .. versionadded:: 2017.7.0

    :param host: technical name of the host
    :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

        add_zabbix_templates_to_host:
            zabbix_host.assign_templates:
                - host: TestHost
                - templates:
                    - "Template OS Linux"
                    - "Template App MySQL"

    '''
    connection_args = {}
    if '_connection_user' in kwargs:
        connection_args['_connection_user'] = kwargs['_connection_user']
    if '_connection_password' in kwargs:
        connection_args['_connection_password'] = kwargs['_connection_password']
    if '_connection_url' in kwargs:
        connection_args['_connection_url'] = kwargs['_connection_url']

    ret = {'name': host, 'changes': {}, 'result': False, 'comment': ''}

    # Set comments
    comment_host_templates_updated = 'Templates updated.'
    comment_host_templ_notupdated = 'Unable to update templates on host: {0}.'.format(host)
    comment_host_templates_in_sync = 'Templates already synced.'

    update_host_templates = False
    curr_template_ids = list()
    requested_template_ids = list()
    hostid = ''

    host_exists = __salt__['zabbix.host_exists'](host, **connection_args)

    # Fail out if host does not exist
    if not host_exists:
        ret['result'] = False
        ret['comment'] = comment_host_templ_notupdated
        return ret

    host_info = __salt__['zabbix.host_get'](host=host, **connection_args)[0]
    hostid = host_info['hostid']

    if not templates:
        templates = list()

    # Get current templateids for host
    host_templates = __salt__['zabbix.host_get'](hostids=hostid,
                                                 output='[{"hostid"}]',
                                                 selectParentTemplates='["templateid"]',
                                                 **connection_args)
    for template_id in host_templates[0]['parentTemplates']:
        curr_template_ids.append(template_id['templateid'])

    # Get requested templateids
    for template in templates:
        try:
            template_id = __salt__['zabbix.template_get'](host=template, **connection_args)[0]['templateid']
            requested_template_ids.append(template_id)
        except TypeError:
            ret['result'] = False
            ret['comment'] = 'Unable to find template: {0}.'.format(template)
            return ret

    # remove any duplications
    requested_template_ids = list(set(requested_template_ids))

    if set(curr_template_ids) != set(requested_template_ids):
        update_host_templates = True

    # Set change output
    changes_host_templates_modified = {host: {'old': 'Host templates: ' + ", ".join(curr_template_ids),
                                              'new': 'Host templates: ' + ', '.join(requested_template_ids)}}

    # Dry run, test=true mode
    if __opts__['test']:
        if update_host_templates:
            ret['result'] = None
            ret['comment'] = comment_host_templates_updated
        else:
            ret['result'] = True
            ret['comment'] = comment_host_templates_in_sync
        return ret

    # Attempt to perform update
    ret['result'] = True
    if update_host_templates:
        update_output = __salt__['zabbix.host_update'](hostid, templates=(requested_template_ids), **connection_args)
        if update_output is False:
            ret['result'] = False
            ret['comment'] = comment_host_templ_notupdated
            return ret
        ret['comment'] = comment_host_templates_updated
        ret['changes'] = changes_host_templates_modified
    else:
        ret['comment'] = comment_host_templates_in_sync

    return ret