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

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

    del CHANGE_STACK[:]

    # 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: %s', 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