diff options
Diffstat (limited to 'ansible_collections/community/zabbix/plugins')
8 files changed, 952 insertions, 201 deletions
diff --git a/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py b/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py index 56ba0356d..2a8d8a8ae 100644 --- a/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py +++ b/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py @@ -29,7 +29,8 @@ options: zabbix_url_path: type: str description: - - Specifies path portion in Zabbix WebUI URL, e.g. for https://myzabbixfarm.com/zabbixeu zabbix_url_path=zabbixeu + - Specifies path portion in Zabbix WebUI URL, e.g. for https://myzabbixfarm.com/zabbixeu zabbix_url_path=zabbixeu. + - "If Zabbix WebUI is running at the root, i.e. https://myzabbixfarm.com/, then assign empty string to this variable C(zabbix_url_path: '')." default: zabbix env: - name: ANSIBLE_ZABBIX_URL_PATH diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_correlation.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_correlation.py new file mode 100644 index 000000000..952ef49d0 --- /dev/null +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_correlation.py @@ -0,0 +1,587 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2024, ONODERA Masaru <masaru-onodera@ieee.org> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: zabbix_correlation + +short_description: Create/update/delete Zabbix correlation + + +description: + - This module allows you to create, update and delete Zabbix correlation. + +author: + - ONODERA Masaru(@masa-orca) + +requirements: + - "python >= 3.9" + +version_added: 2.4.0 + +options: + name: + description: + - Name of this correlation + type: str + required: true + description: + description: + - Description of this correlation + type: str + operations: + description: + - A operations. + type: list + elements: dict + suboptions: + type: + description: + - Type of the operation. + type: str + choices: + - close_old_events + - close_new_event + required: true + filter: + description: + - A filter. + type: dict + suboptions: + evaltype: + description: + - An evaluation method of the filter. + type: str + choices: + - and_or + - and + - or + - custom_expression + required: true + conditions: + description: + - A condition. + type: list + elements: dict + required: true + suboptions: + type: + description: + - Type of the condition. + type: str + choices: + - old_event_tag + - new_event_tag + - new_event_host_group + - event_tag_pair + - old_event_tag_value + - new_event_tag_value + required: true + tag: + description: + - Event tag (old or new). + - This parameter is required if value of I(type) of the condition is C(old_event_tag), C(new_event_tag), + C(old_event_tag_value) or C(new_event_tag_value). + type: str + hostgroup: + description: + - A name of hostgroup name + - This parameter is required if value of I(type) of the condition is C(new_event_host_group). + type: str + oldtag: + description: + - Old event tag. + - This parameter is required if value of I(type) of the condition is C(event_tag_pair). + type: str + newtag: + description: + - New event tag. + - This parameter is required if value of I(type) of the condition is C(event_tag_pair). + type: str + value: + description: + - Event tag (old or new) value. + - This parameter is required if value of I(type) of the condition is C(old_event_tag_value) or C(new_event_tag_value). + type: str + formulaid: + description: + - Arbitrary unique ID that is used to reference the condition from a custom expression. + - This parameter's value must be only contain capital-case letters. + - This parameter is required if value of I(evaltype) of the filter is C(custom_expression). + type: str + operator: + description: + - Condition operator. + - This parameter is required if value of I(type) of the condition is C(new_event_host_group), + C(old_event_tag_value) or C(new_event_tag_value). + - You can only select C(equal) or C(not_equal) if value of I(type) of the condition is C(new_event_host_group). + type: str + choices: + - equal + - not_equal + - like + - not_like + formula: + description: + - A custom expression for the filter. + - This parameter is required if value of I(evaltype) is C(custom_expression). + type: str + status: + description: + - Status of the correlation. + choices: + - enabled + - disabled + default: enabled + type: str + state: + description: + - State of the correlation. + type: str + choices: + - present + - absent + default: present + +extends_documentation_fragment: + - community.zabbix.zabbix + +""" + +EXAMPLES = """ +# If you want to use Username and Password to be authenticated by Zabbix Server +- name: Set credentials to access Zabbix Server API + ansible.builtin.set_fact: + ansible_user: Admin + ansible_httpapi_pass: zabbix + +# If you want to use API token to be authenticated by Zabbix Server +# https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/administration/general#api-tokens +- name: Set API token + ansible.builtin.set_fact: + ansible_zabbix_auth_key: 8ec0d52432c15c91fcafe9888500cf9a607f44091ab554dbee860f6b44fac895 + +- name: Create correlation of 'New event tag correlation' + # set task level variables as we change ansible_connection plugin here + vars: + ansible_network_os: community.zabbix.zabbix + ansible_connection: httpapi + ansible_httpapi_port: 443 + ansible_httpapi_use_ssl: true + ansible_httpapi_validate_certs: false + ansible_zabbix_url_path: 'zabbixeu' # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu + ansible_host: zabbix-example-fqdn.org + community.zabbix.zabbix_correlation: + name: New event tag correlation + filter: + evaltype: and_or + conditions: + - type: new_event_tag + tag: ok + operations: + - type: close_old_events +""" + +RETURN = """ +msg: + description: The result of the operation + returned: success + type: str + sample: 'Successfully created correlation' +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase +import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils + + +class Correlation(ZabbixBase): + def __init__(self, module, zbx=None, zapi_wrapper=None): + super(Correlation, self).__init__(module, zbx, zapi_wrapper) + + def get_correlations(self, correlation_name): + try: + regexps = self._zapi.correlation.get( + { + "output": "extend", + "selectFilter": "extend", + "selectOperations": "extend", + "filter": {"name": correlation_name}, + } + ) + if len(regexps) >= 2: + self._module.fail_json("Too many correlations are matched.") + return regexps + except Exception as e: + self._module.fail_json( + msg="Failed to get correlation: %s" % e + ) + + def delete_correlation(self, correlation): + try: + parameter = [correlation["correlationid"]] + if self._module.check_mode: + self._module.exit_json(changed=True) + self._zapi.correlation.delete(parameter) + self._module.exit_json( + changed=True, msg="Successfully deleted correlation." + ) + except Exception as e: + self._module.fail_json( + msg="Failed to delete correlation: %s" % e + ) + + def _convert_operations_to_json(self, operations): + operation_type_values = ["close_old_events", "close_new_event"] + + operations_json = [] + for operation in operations: + operation_json = {} + operation_type = zabbix_utils.helper_to_numeric_value( + operation_type_values, operation["type"] + ) + operation_json["type"] = str(operation_type) + operations_json.append(operation_json) + + return operations_json + + def _get_groupid_from_name(self, hostgroup): + groupid = self._zapi.hostgroup.get({"filter": {"name": hostgroup}}) + if not groupid or len(groupid) > 1: + self._module.fail_json("Host group '%s' cannot be found" % hostgroup) + return groupid[0]["groupid"] + + def _convert_conditions_to_json(self, filter_parameter): + condition_type_values = [ + "old_event_tag", + "new_event_tag", + "new_event_host_group", + "event_tag_pair", + "old_event_tag_value", + "new_event_tag_value" + ] + + operator_values = [ + "equal", + "not_equal", + "like", + "not_like" + ] + + conditions_json = [] + for condition in filter_parameter["conditions"]: + condition_json = {} + + condition_type = zabbix_utils.helper_to_numeric_value( + condition_type_values, condition["type"] + ) + condition_json["type"] = str(condition_type) + + if condition["tag"] is not None: + condition_json["tag"] = condition["tag"] + + if condition["hostgroup"] is not None: + condition_json["groupid"] = self._get_groupid_from_name(condition["hostgroup"]) + + if condition["oldtag"] is not None: + condition_json["oldtag"] = condition["oldtag"] + + if condition["newtag"] is not None: + condition_json["newtag"] = condition["newtag"] + + if condition["value"] is not None: + condition_json["value"] = condition["value"] + + if filter_parameter["evaltype"] == "custom_expression": + if condition["formulaid"] is not None: + if not condition["formulaid"].isupper(): + self._module.fail_json( + "A value of formulaid must be uppercase." + ) + condition_json["formulaid"] = condition["formulaid"] + else: + self._module.fail_json( + "formulaid must be defined if evaltype is 'custom_expression'." + ) + else: + if condition["formulaid"] is not None: + self._module.warn( + "A value of formulaid will be ignored because evaltype is not 'custom_expression'." + ) + + if condition["operator"] is not None: + if (condition["type"] == "new_event_host_group" + and (condition["operator"] == "like" or condition["operator"] == "not_like")): + self._module.fail_json( + "A value of operator must be equal or not_equal when condition's type is 'new_event_host_group'." + ) + operator = zabbix_utils.helper_to_numeric_value( + operator_values, condition["operator"] + ) + condition_json["operator"] = str(operator) + + conditions_json.append(condition_json) + return conditions_json + + def _convert_filter_parameter_to_json(self, filter_parameter): + evaltype_values = [ + "and_or", + "and", + "or", + "custom_expression" + ] + + filter_parameter_json = {} + + evaltype = zabbix_utils.helper_to_numeric_value( + evaltype_values, filter_parameter["evaltype"] + ) + filter_parameter_json["evaltype"] = str(evaltype) + + filter_parameter_json["conditions"] = self._convert_conditions_to_json(filter_parameter) + + if filter_parameter["formula"] is not None: + if filter_parameter["evaltype"] == "custom_expression": + filter_parameter_json["formula"] = filter_parameter["formula"] + else: + self._module.warn( + "A value of formula will be ignored because evaltype is not 'custom_expression'." + ) + + return filter_parameter_json + + def create_correlation(self, name, description, operations, filter_parameter, status): + status_values = ["enabled", "disabled"] + status_json = zabbix_utils.helper_to_numeric_value( + status_values, status + ) + + try: + correlation_json = {} + + correlation_json["name"] = name + + if description is not None: + correlation_json["description"] = description + + correlation_json["operations"] = self._convert_operations_to_json(operations) + + correlation_json["filter"] = self._convert_filter_parameter_to_json(filter_parameter) + + correlation_json["status"] = status_json + + if self._module.check_mode: + self._module.exit_json(changed=True) + self._zapi.correlation.create(correlation_json) + self._module.exit_json( + changed=True, msg="Successfully created correlation" + ) + except Exception as e: + self._module.fail_json( + msg="Failed to create correlation: %s" % e + ) + + def check_filter_properties(self, current_filter, future_filter): + changed = False + + if (current_filter["evaltype"] != future_filter["evaltype"]): + changed = True + + if "formula" in future_filter.keys(): + if (current_filter["eval_formula"] != future_filter["formula"]): + changed = True + + for condition in current_filter["conditions"]: + # 3 means custom expression. + if current_filter["evaltype"] != "3": + condition.pop("formulaid") + diff_conditions = [] + zabbix_utils.helper_compare_lists(current_filter["conditions"], future_filter["conditions"], diff_conditions) + if len(diff_conditions) != 0: + changed = True + + return changed + + def update_correlation(self, current_correlation, description, operations, filter_parameter, status): + status_values = ["enabled", "disabled"] + status_json = zabbix_utils.helper_to_numeric_value( + status_values, status + ) + + try: + correlation_json = {} + + if description is not None and description != current_correlation["description"]: + correlation_json["description"] = description + + if operations is not None: + future_operations = self._convert_operations_to_json(operations) + diff_operations = [] + zabbix_utils.helper_compare_lists(current_correlation["operations"], future_operations, diff_operations) + if len(diff_operations) != 0: + correlation_json["operations"] = future_operations + + if filter_parameter is not None: + future_filter = self._convert_filter_parameter_to_json(filter_parameter) + if self.check_filter_properties(current_correlation["filter"], future_filter): + correlation_json["filter"] = future_filter + + if str(status_json) != current_correlation["status"]: + correlation_json["status"] = str(status_json) + + if len(correlation_json.keys()) == 0: + self._module.exit_json(changed=False) + else: + correlation_json["correlationid"] = current_correlation["correlationid"] + if self._module.check_mode: + self._module.exit_json(changed=True) + self._zapi.correlation.update(correlation_json) + self._module.exit_json( + changed=True, msg="Successfully updated correlation" + ) + except Exception as e: + self._module.fail_json( + msg="Failed to update correlation: %s" % e + ) + + +def main(): + """Main ansible module function""" + + argument_spec = zabbix_utils.zabbix_common_argument_spec() + argument_spec.update( + dict( + name=dict(type="str", required=True), + description=dict(type="str"), + operations=dict( + type="list", + elements="dict", + options=dict( + type=dict( + choices=["close_old_events", "close_new_event"], + required=True + ) + ) + ), + filter=dict( + type="dict", + options=dict( + evaltype=dict( + type="str", + required=True, + choices=[ + "and_or", + "and", + "or", + "custom_expression" + ] + ), + conditions=dict( + required=True, + type="list", + elements="dict", + options=dict( + type=dict( + type="str", + required=True, + choices=[ + "old_event_tag", + "new_event_tag", + "new_event_host_group", + "event_tag_pair", + "old_event_tag_value", + "new_event_tag_value" + ] + ), + tag=dict(type="str"), + hostgroup=dict(type="str"), + oldtag=dict(type="str"), + newtag=dict(type="str"), + value=dict(type="str"), + formulaid=dict(type="str"), + operator=dict( + type="str", + choices=[ + "equal", + "not_equal", + "like", + "not_like" + ] + ), + ), + required_if=[ + ["type", "old_event_tag", ["tag"]], + ["type", "new_event_tag", ["tag"]], + ["type", "new_event_host_group", ["hostgroup", "operator"]], + ["type", "event_tag_pair", ["oldtag", "newtag"]], + ["type", "old_event_tag_value", ["tag", "value", "operator"]], + ["type", "new_event_tag_value", ["tag", "value", "operator"]] + ], + mutually_exclusive=[ + ("tag", "hostgroup", "oldtag") + ], + required_together=[ + ('oldtag', 'newtag') + ] + ), + formula=dict(type="str"), + ), + required_if=[ + ["evaltype", "custom_expression", ["formula"]], + ] + ), + status=dict( + type="str", + required=False, + default="enabled", + choices=["enabled", "disabled"], + ), + state=dict( + type="str", + required=False, + default="present", + choices=["present", "absent"], + ) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + name = module.params["name"] + description = module.params["description"] + operations = module.params["operations"] + filter_parameter = module.params["filter"] + status = module.params["status"] + state = module.params["state"] + + correlation_class_obj = Correlation(module) + correlations = correlation_class_obj.get_correlations(name) + + if state == "absent": + if len(correlations) == 1: + correlation_class_obj.delete_correlation(correlations[0]) + else: + module.exit_json(changed=False) + else: + if len(correlations) == 1: + correlation_class_obj.update_correlation(correlations[0], description, operations, filter_parameter, status) + else: + correlation_class_obj.create_correlation(name, description, operations, filter_parameter, status) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_host.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_host.py index 22e14d17d..00c04c221 100644 --- a/ansible_collections/community/zabbix/plugins/modules/zabbix_host.py +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_host.py @@ -513,7 +513,7 @@ class Host(ZabbixBase): def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id, visible_name, description, tls_connect, tls_accept, tls_psk_identity, tls_psk, tls_issuer, - tls_subject, ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, macros, tags, discovered_host): + tls_subject, ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, macros, tags, discovered_host, zabbix_host_obj): try: if self._module.check_mode: self._module.exit_json(changed=True) @@ -523,31 +523,31 @@ class Host(ZabbixBase): else: # A "plain" host parameters = {"hostid": host_id, "groups": group_ids, "status": status} - if proxy_id >= 0: + if (proxy_id >= 0 and proxy_id != zabbix_host_obj["proxy_hostid"]): parameters["proxy_hostid"] = proxy_id - if visible_name: + if (visible_name is not None and visible_name != zabbix_host_obj["name"]): parameters["name"] = visible_name - if tls_connect: + if (tls_connect is not None and tls_connect != zabbix_host_obj["tls_connect"]): parameters["tls_connect"] = tls_connect - if tls_accept: + if (tls_accept is not None and tls_accept != zabbix_host_obj["tls_accept"]): parameters["tls_accept"] = tls_accept if tls_psk_identity: parameters["tls_psk_identity"] = tls_psk_identity if tls_psk: parameters["tls_psk"] = tls_psk - if tls_issuer: + if (tls_issuer is not None and tls_issuer != zabbix_host_obj["tls_issuer"]): parameters["tls_issuer"] = tls_issuer - if tls_subject: + if (tls_subject is not None and tls_subject != zabbix_host_obj["tls_subject"]): parameters["tls_subject"] = tls_subject - if description: + if (description is not None and description != zabbix_host_obj["description"]): parameters["description"] = description - if ipmi_authtype: + if (ipmi_authtype is not None and ipmi_authtype != zabbix_host_obj["ipmi_authtype"]): parameters["ipmi_authtype"] = ipmi_authtype - if ipmi_privilege: + if (ipmi_privilege is not None and ipmi_privilege != zabbix_host_obj["ipmi_privilege"]): parameters["ipmi_privilege"] = ipmi_privilege - if ipmi_username: + if (ipmi_username is not None and ipmi_username != zabbix_host_obj["ipmi_username"]): parameters["ipmi_username"] = ipmi_username - if ipmi_password: + if (ipmi_password is not None and ipmi_password != zabbix_host_obj["ipmi_password"]): parameters["ipmi_password"] = ipmi_password if interfaces: parameters["interfaces"] = interfaces @@ -809,8 +809,7 @@ class Host(ZabbixBase): return False # link or clear template of the host - def link_or_clear_template(self, host_id, template_id_list, tls_connect, tls_accept, tls_psk_identity, tls_psk, - tls_issuer, tls_subject, ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, discovered_host): + def link_or_clear_template(self, host_id, template_id_list): # get host's exist template ids exist_template_id_list = self.get_host_templates_by_host_id(host_id) @@ -821,25 +820,7 @@ class Host(ZabbixBase): # get unlink and clear templates templates_clear = exist_template_ids.difference(template_ids) templates_clear_list = list(templates_clear) - if discovered_host: - # The host was discovered via Discovery Rule - request_str = {"hostid": host_id, "templates": template_id_list, "templates_clear": templates_clear_list} - else: - # A "plain" host - request_str = {"hostid": host_id, "templates": template_id_list, "templates_clear": templates_clear_list, - "ipmi_authtype": ipmi_authtype, "ipmi_privilege": ipmi_privilege, "ipmi_username": ipmi_username, "ipmi_password": ipmi_password} - if tls_connect: - request_str["tls_connect"] = tls_connect - if tls_accept: - request_str["tls_accept"] = tls_accept - if tls_psk_identity is not None: - request_str["tls_psk_identity"] = tls_psk_identity - if tls_psk is not None: - request_str["tls_psk"] = tls_psk - if tls_issuer is not None: - request_str["tls_issuer"] = tls_issuer - if tls_subject is not None: - request_str["tls_subject"] = tls_subject + request_str = {"hostid": host_id, "templates": template_id_list, "templates_clear": templates_clear_list} try: if self._module.check_mode: self._module.exit_json(changed=True) @@ -1057,8 +1038,11 @@ def main(): group_ids = [] - if host_groups: - group_ids = host.get_group_ids_by_group_names(host_groups) + if host_groups is not None: + if len(host_groups) >= 1: + group_ids = host.get_group_ids_by_group_names(host_groups) + else: + module.fail_json(msg="host_groups must be not empty list.") interfaces, ip = host.construct_host_interfaces(interfaces) @@ -1204,11 +1188,9 @@ def main(): host.update_host( host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id, visible_name, description, tls_connect, tls_accept, tls_psk_identity, tls_psk, tls_issuer, tls_subject, - ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, macros, tags, discovered_host) + ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, macros, tags, discovered_host, zabbix_host_obj) - host.link_or_clear_template( - host_id, template_ids, tls_connect, tls_accept, tls_psk_identity, tls_psk, tls_issuer, - tls_subject, ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, discovered_host) + host.link_or_clear_template(host_id, template_ids) host.update_inventory_mode(host_id, inventory_mode) host.update_inventory_zabbix(host_id, inventory_zabbix) @@ -1235,9 +1217,7 @@ def main(): tls_psk_identity, tls_psk, tls_issuer, tls_subject, ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, macros, tags) - host.link_or_clear_template( - host_id, template_ids, tls_connect, tls_accept, tls_psk_identity, tls_psk, tls_issuer, tls_subject, - ipmi_authtype, ipmi_privilege, ipmi_username, ipmi_password, discovered_host) + host.link_or_clear_template(host_id, template_ids) host.update_inventory_mode(host_id, inventory_mode) host.update_inventory_zabbix(host_id, inventory_zabbix) diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_maintenance.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_maintenance.py index a2c635eb0..ce10815df 100644 --- a/ansible_collections/community/zabbix/plugins/modules/zabbix_maintenance.py +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_maintenance.py @@ -64,6 +64,20 @@ options: - I(visible_name=yes) to search by visible name, I(visible_name=no) to search by technical name. type: bool default: "yes" + active_since: + description: + - Time when the maintenance becomes active. + - The given value will be rounded down to minutes. + - Uses `datetime.datetime.now(`) if not specified. + type: "str" + default: "" + active_till: + description: + - Time when the maintenance stops being active. + - The given value will be rounded down to minutes. + - Gets calculated from I(minutes) if not specified. + type: "str" + default: "" tags: description: - List of tags to assign to the hosts in maintenance. @@ -187,6 +201,24 @@ EXAMPLES = r""" community.zabbix.zabbix_maintenance: name: Test1 state: absent + +- name: Create maintenance window by date + # set task level variables as we change ansible_connection plugin here + vars: + ansible_network_os: community.zabbix.zabbix + ansible_connection: httpapi + ansible_httpapi_port: 443 + ansible_httpapi_use_ssl: true + ansible_httpapi_validate_certs: false + ansible_zabbix_url_path: "zabbixeu" # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu + ansible_host: zabbix-example-fqdn.org + community.zabbix.zabbix_maintenance: + name: TestDate + state: present + host_names: + - host.example.org + active_since: "1979-09-19 09:00" + active_till: "1979-09-19 17:00" """ import datetime @@ -342,6 +374,8 @@ def main(): desc=dict(type="str", required=False, default="Created by Ansible"), collect_data=dict(type="bool", required=False, default=True), visible_name=dict(type="bool", required=False, default=True), + active_since=dict(type="str", required=False, default=""), + active_till=dict(type="str", required=False, default=""), tags=dict( type="list", elements="dict", @@ -368,6 +402,8 @@ def main(): desc = module.params["desc"] collect_data = module.params["collect_data"] visible_name = module.params["visible_name"] + active_since = module.params["active_since"] + active_till = module.params["active_till"] tags = module.params["tags"] if collect_data: @@ -389,9 +425,9 @@ def main(): module.fail_json( msg="At least one host_name or host_group must be defined for each created maintenance.") - now = datetime.datetime.now().replace(second=0) + now = datetime.datetime.fromisoformat(active_since) if active_since != "" else datetime.datetime.now().replace(second=0) start_time = int(time.mktime(now.timetuple())) - period = 60 * int(minutes) # N * 60 seconds + period = int((datetime.datetime.fromisoformat(active_till) - now).total_seconds()) if active_till != "" else 60 * int(minutes) # N * 60 seconds if host_groups: (rc, group_ids, error) = maint.get_group_ids(host_groups) diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_mediatype.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_mediatype.py index 1e2514bf3..34bb1db82 100644 --- a/ansible_collections/community/zabbix/plugins/modules/zabbix_mediatype.py +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_mediatype.py @@ -155,6 +155,15 @@ options: - SSL verify peer for SMTP. - Can be specified when I(smtp_security=STARTTLS) or I(smtp_security=SSL/TLS) default: false + content_type: + type: "str" + description: + - Can be used when I(type=email). + - Message format. + choices: + - plaintext + - html + default: html message_text_limit: type: "str" description: @@ -525,6 +534,7 @@ class MediaTypeModule(ZabbixBase): smtp_authentication=truths.get(str(self._module.params["smtp_authentication"])), smtp_verify_host=truths.get(str(self._module.params["smtp_verify_host"])), smtp_verify_peer=truths.get(str(self._module.params["smtp_verify_peer"])), + content_type={"plaintext": "0", "html": "1"}.get(str(self._module.params["content_type"])), username=self._module.params["username"], passwd=self._module.params["password"] )) @@ -693,6 +703,7 @@ def main(): smtp_authentication=dict(type="bool", default=False, required=False), smtp_verify_host=dict(type="bool", default=False, required=False), smtp_verify_peer=dict(type="bool", default=False, required=False), + content_type=dict(type="str", choices=["plaintext", "html"], default="html", required=False), # EZ Text message_text_limit=dict(type="str", required=False, choices=["USA", "Canada"]), # Webhook diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_service_info.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_service_info.py new file mode 100644 index 000000000..1984777d3 --- /dev/null +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_service_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, D3DeFi +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +module: zabbix_service_info +short_description: Gather information about Zabbix service +author: + - Kanstantsin Maksimau (@us3241) +description: + - This module allows you to obtain detailed information about configured zabbix service. +requirements: + - "python >= 3.9" +options: + service_name: + description: + - Name of the Zabbix service. + required: true + type: str +extends_documentation_fragment: +- community.zabbix.zabbix + +""" + +EXAMPLES = """ +# If you want to use Username and Password to be authenticated by Zabbix Server +- name: Set credentials to access Zabbix Server API + ansible.builtin.set_fact: + ansible_user: Admin + ansible_httpapi_pass: zabbix + +# If you want to use API token to be authenticated by Zabbix Server +# https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/administration/general#api-tokens +- name: Set API token + ansible.builtin.set_fact: + ansible_zabbix_auth_key: 8ec0d52432c15c91fcafe9888500cf9a607f44091ab554dbee860f6b44fac895 + +- name: Get zabbix service info + # set task level variables as we change ansible_connection plugin here + vars: + ansible_network_os: community.zabbix.zabbix + ansible_connection: httpapi + ansible_httpapi_port: 443 + ansible_httpapi_use_ssl: true + ansible_httpapi_validate_certs: false + ansible_zabbix_url_path: "zabbixeu" # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu + ansible_host: zabbix-example-fqdn.org + community.zabbix.zabbix_service_info: + service_name: zbx01.example.com +""" + +RETURN = """ +zabbix_service: + description: example + returned: always + type: dict + sample: { + "algorithm": "2", + "children": [ + { + "algorithm": "0", + "created_at": "1712743194", + "description": "", + "name": "example children", + "propagation_rule": "0", + "propagation_value": "0", + "readonly": false, + "serviceid": "81", + "sortorder": "0", + "status": "-1", + "uuid": "ce6e1a3784a547b48ee6707f6e061102", + "weight": "0" + } + ], + "created_at": "1709717864", + "description": "", + "name": "example service", + "parents": [], + "problem_tags": [], + "propagation_rule": "0", + "propagation_value": "0", + "readonly": false, + "serviceid": "51", + "sortorder": "0", + "status": "-1", + "status_rules": [], + "tags": [], + "uuid": "420e48b363fe473c95288c817031447f", + "weight": "0" + } +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase +import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils + + +class Service(ZabbixBase): + + def get_service(self, name): + result = {} + params = { + "filter": { + "name": name + }, + "output": "extend", + "selectParents": "extend", + "selectTags": "extend", + "selectProblemTags": "extend", + "selectChildren": "extend", + "selectStatusRules": "extend" + } + + try: + result = self._zapi.service.get(params) + except Exception as e: + self._module.fail_json(msg="Failed to get service information: %s" % e) + + return result[0] if result else {} + + +def main(): + argument_spec = zabbix_utils.zabbix_common_argument_spec() + argument_spec.update(dict( + service_name=dict(type="str", required=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + name = module.params["service_name"] + + service = Service(module) + result = service.get_service(name) + module.exit_json(changed=False, zabbix_service=result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_template.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_template.py index a773790aa..f2c19c33c 100644 --- a/ansible_collections/community/zabbix/plugins/modules/zabbix_template.py +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_template.py @@ -33,17 +33,21 @@ options: description: - JSON dump of templates to import. - Multiple templates can be imported this way. - - Mutually exclusive with I(template_name) and I(template_xml). + - Mutually exclusive with I(template_name) and I(template_xml) and I(template_yaml). required: false type: json template_xml: description: - XML dump of templates to import. - Multiple templates can be imported this way. - - You are advised to pass XML structure matching the structure used by your version of Zabbix server. - - Custom XML structure can be imported as long as it is valid, but may not yield consistent idempotent - results on subsequent runs. - - Mutually exclusive with I(template_name) and I(template_json). + - Mutually exclusive with I(template_name) and I(template_json) and I(template_yaml). + required: false + type: str + template_yaml: + description: + - Context of exported templates file to import. + - Multiple templates can be imported this way. + - Mutually exclusive with I(template_name) and I(template_json) and I(template_xml). required: false type: str template_groups: @@ -288,7 +292,6 @@ RETURN = r""" import json import traceback import re -import xml.etree.ElementTree as ET from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native @@ -342,35 +345,84 @@ class Template(ZabbixBase): self._zapi.template.create(new_template) - def check_template_changed(self, template_ids, template_groups, link_templates, clear_templates, - template_macros, template_tags, template_content, template_type): - """Compares template parameters to already existing values if any are found. + def import_compare(self, template_content, template_type): + """template_content has same structure as Zabbix uses (e.g. it was optimally exported via Zabbix GUI or API)""" - template_json - JSON structures are compared as deep sorted dictionaries, - template_xml - XML structures are compared as strings, but filtered and formatted first, - If none above is used, all the other arguments are compared to their existing counterparts - retrieved from Zabbix API.""" changed = False - # Compare filtered and formatted XMLs strings for any changes. It is expected that provided - # XML has same structure as Zabbix uses (e.g. it was optimally exported via Zabbix GUI or API) - if template_content is not None and template_type == "xml": - existing_template = self.dump_template(template_ids, template_type="xml") - - if self.filter_xml_template(template_content) != self.filter_xml_template(existing_template): - changed = True + if template_content is not None: + update_rules = { + "discoveryRules": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "graphs": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "host_groups": { + "createMissing": True + }, + "httptests": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "items": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "templates": { + "createMissing": True, + "updateExisting": True + }, + "template_groups": { + "createMissing": True + }, + "templateLinkage": { + "createMissing": True + }, + "templateScreens": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "triggers": { + "createMissing": True, + "updateExisting": True, + "deleteMissing": True + }, + "valueMaps": { + "createMissing": True, + "updateExisting": True + } + } - return changed + try: + update_rules["templateLinkage"]["deleteMissing"] = True + update_rules["templateDashboards"] = update_rules.pop("templateScreens") + + # before Zabbix 6.2 host_groups and template_group are joined into groups parameter + if LooseVersion(self._zbx_api_version) < LooseVersion("6.2"): + update_rules["groups"] = {"createMissing": True} + update_rules.pop("host_groups", None) + update_rules.pop("template_groups", None) + importcompare = {"format": template_type, "source": template_content, "rules": update_rules} + compare_result = self._zapi.configuration.importcompare(importcompare) + if len(compare_result) != 0: + changed = True + return changed + except Exception as e: + self._module.fail_json(msg="Unable to compare template", details=to_native(e), + exception=traceback.format_exc()) + def check_template_changed(self, template_ids, template_groups, link_templates, clear_templates, + template_macros, template_tags): + """Compare template with user provided all parameters via module options""" + changed = False existing_template = self.dump_template(template_ids, template_type="json") - # Compare JSON objects as deep sorted python dictionaries - if template_content is not None and template_type == "json": - parsed_template_json = self.load_json_template(template_content) - if self.diff_template(parsed_template_json, existing_template): - changed = True - - return changed - - # If neither template_json or template_xml were used, user provided all parameters via module options if template_groups is not None: if LooseVersion(self._zbx_api_version) >= LooseVersion("6.2"): existing_groups = [g["name"] for g in existing_template["zabbix_export"]["template_groups"]] @@ -451,81 +503,15 @@ class Template(ZabbixBase): templateids_list = [t.get("templateid") for t in templateids] self._zapi.template.delete(templateids_list) - def ordered_json(self, obj): - # Deep sort json dicts for comparison - if isinstance(obj, dict): - return sorted((k, self.ordered_json(v)) for k, v in obj.items()) - if isinstance(obj, list): - return sorted(self.ordered_json(x) for x in obj) - else: - return obj - def dump_template(self, template_ids, template_type="json"): template_ids_list = [t.get("templateid") for t in template_ids] try: dump = self._zapi.configuration.export({"format": template_type, "options": {"templates": template_ids_list}}) - if template_type == "xml": - xmlroot = ET.fromstring(dump.encode("utf-8")) - if PY2: - return str(ET.tostring(xmlroot, encoding="utf-8")) - else: - return str(ET.tostring(xmlroot, encoding="utf-8").decode("utf-8")) - else: - return self.load_json_template(dump) + return self.load_json_template(dump) except Exception as e: self._module.fail_json(msg="Unable to export template: %s" % e) - def diff_template(self, template_json_a, template_json_b): - # Compare 2 zabbix templates and return True if they differ. - template_json_a = self.filter_template(template_json_a) - template_json_b = self.filter_template(template_json_b) - if self.ordered_json(template_json_a) == self.ordered_json(template_json_b): - return False - return True - - def filter_template(self, template_json): - # Filter the template json to contain only the keys we will update - keep_keys = set(["graphs", "templates", "triggers", "value_maps"]) - unwanted_keys = set(template_json["zabbix_export"]) - keep_keys - for unwanted_key in unwanted_keys: - del template_json["zabbix_export"][unwanted_key] - - # Versions older than 2.4 do not support description field within template - desc_not_supported = False - - # Filter empty attributes from template object to allow accurate comparison - for template in template_json["zabbix_export"]["templates"]: - for key in list(template.keys()): - if not template[key] or (key == "description" and desc_not_supported): - template.pop(key) - - return template_json - - def filter_xml_template(self, template_xml): - """Filters out keys from XML template that may wary between exports (e.g date or version) and - keys that are not imported via this module. - - It is advised that provided XML template exactly matches XML structure used by Zabbix""" - # Strip last new line and convert string to ElementTree - parsed_xml_root = self.load_xml_template(template_xml.strip()) - keep_keys = ["graphs", "templates", "triggers", "value_maps"] - - # Remove unwanted XML nodes - for node in list(parsed_xml_root): - if node.tag not in keep_keys: - parsed_xml_root.remove(node) - - # Filter empty attributes from template objects to allow accurate comparison - for template in list(parsed_xml_root.find("templates")): - for element in list(template): - if element.text is None and len(list(element)) == 0: - template.remove(element) - - # Filter new lines and indentation - xml_root_text = list(line.strip() for line in ET.tostring(parsed_xml_root, encoding="utf8", method="xml").decode().split("\n")) - return "".join(xml_root_text) - def load_json_template(self, template_json): try: jsondoc = json.loads(template_json) @@ -533,12 +519,6 @@ class Template(ZabbixBase): except ValueError as e: self._module.fail_json(msg="Invalid JSON provided", details=to_native(e), exception=traceback.format_exc()) - def load_xml_template(self, template_xml): - try: - return ET.fromstring(template_xml) - except ET.ParseError as e: - self._module.fail_json(msg="Invalid XML provided", details=to_native(e), exception=traceback.format_exc()) - def import_template(self, template_content, template_type="json"): if self._module.check_mode: self._module.exit_json(changed=True) @@ -624,6 +604,7 @@ def main(): template_name=dict(type="str", required=False), template_json=dict(type="json", required=False), template_xml=dict(type="str", required=False), + template_yaml=dict(type="str", required=False), template_groups=dict(type="list", required=False, elements="str"), link_templates=dict(type="list", required=False, elements="str"), clear_templates=dict(type="list", required=False, elements="str"), @@ -648,10 +629,10 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, required_one_of=[ - ["template_name", "template_json", "template_xml"] + ["template_name", "template_json", "template_xml", "template_yaml"] ], mutually_exclusive=[ - ["template_name", "template_json", "template_xml"] + ["template_name", "template_json", "template_xml", "template_yaml"] ], required_if=[ ["state", "absent", ["template_name"]] @@ -662,6 +643,7 @@ def main(): template_name = module.params["template_name"] template_json = module.params["template_json"] template_xml = module.params["template_xml"] + template_yaml = module.params["template_yaml"] template_groups = module.params["template_groups"] link_templates = module.params["link_templates"] clear_templates = module.params["clear_templates"] @@ -673,23 +655,23 @@ def main(): # Identify template names for IDs retrieval # Template names are expected to reside in ["zabbix_export"]["templates"][*]["template"] for both data types - template_content, template_type = None, None + template_content, template_type, template_ids = None, None, None + if template_json is not None: template_type = "json" template_content = template_json - json_parsed = template.load_json_template(template_content) - template_names = list(t["template"] for t in json_parsed["zabbix_export"]["templates"]) elif template_xml is not None: template_type = "xml" template_content = template_xml - xml_parsed = template.load_xml_template(template_content) - template_names = list(t.find("template").text for t in list(xml_parsed.find("templates"))) + + elif template_yaml is not None: + template_type = "yaml" + template_content = template_yaml else: template_names = [template_name] - - template_ids = template.get_template_ids(template_names) + template_ids = template.get_template_ids(template_names) if state == "absent": if not template_ids: @@ -699,58 +681,63 @@ def main(): module.exit_json(changed=True, result="Successfully deleted template %s" % template_name) elif state == "present": - # Load all subelements for template that were provided by user - group_ids = None - if template_groups is not None: - group_ids = template.get_group_ids_by_group_names(template_groups) - - link_template_ids = None - if link_templates is not None: - link_template_ids = template.get_template_ids(link_templates) - - clear_template_ids = None - if clear_templates is not None: - clear_template_ids = template.get_template_ids(clear_templates) - - if template_macros is not None: - # Zabbix configuration.export does not differentiate python types (numbers are returned as strings) - for macroitem in template_macros: - for key in macroitem: - macroitem[key] = str(macroitem[key]) - - if template_tags is not None: - for tagitem in template_tags: - for key in tagitem: - tagitem[key] = str(tagitem[key]) - - if not template_ids: - # Assume new templates are being added when no ID"s were found - if template_content is not None: - template.import_template(template_content, template_type) - module.exit_json(changed=True, result="Template import successful") - + if template_content is not None: + changed = template.import_compare(template_content, template_type) + if not changed: + module.exit_json(changed=changed, result="Template is up-to date") else: + if module.check_mode: + module.exit_json(changed=changed) + template.import_template(template_content, template_type) + module.exit_json(changed=changed, result="Template import successful") + else: + # Load all subelements for template that were provided by user + group_ids = None + if template_groups is not None: + group_ids = template.get_group_ids_by_group_names(template_groups) + + link_template_ids = None + if link_templates is not None: + link_template_ids = template.get_template_ids(link_templates) + + clear_template_ids = None + if clear_templates is not None: + clear_template_ids = template.get_template_ids(clear_templates) + + if template_macros is not None: + # Zabbix configuration.export does not differentiate python types (numbers are returned as strings) + for macroitem in template_macros: + for key in macroitem: + macroitem[key] = str(macroitem[key]) + + if template_tags is not None: + for tagitem in template_tags: + for key in tagitem: + tagitem[key] = str(tagitem[key]) + + if not template_ids: + # Assume new templates are being added when no ID"s were found if group_ids is None: module.fail_json(msg="template_groups are required when creating a new Zabbix template") template.add_template(template_name, group_ids, link_template_ids, template_macros, template_tags) module.exit_json(changed=True, result="Successfully added template: %s" % template_name) - else: - changed = template.check_template_changed(template_ids, template_groups, link_templates, clear_templates, - template_macros, template_tags, template_content, template_type) + else: + changed = template.check_template_changed(template_ids, template_groups, link_templates, clear_templates, + template_macros, template_tags) - if module.check_mode: - module.exit_json(changed=changed) + if module.check_mode: + module.exit_json(changed=changed) - if changed: - if template_type is not None: - template.import_template(template_content, template_type) - else: - template.update_template(template_ids, group_ids, link_template_ids, clear_template_ids, - template_macros, template_tags) + if changed: + if template_type is not None: + template.import_template(template_content, template_type) + else: + template.update_template(template_ids, group_ids, link_template_ids, clear_template_ids, + template_macros, template_tags) - module.exit_json(changed=changed, result="Template successfully updated") + module.exit_json(changed=changed, result="Template successfully updated") if __name__ == "__main__": diff --git a/ansible_collections/community/zabbix/plugins/modules/zabbix_templategroup.py b/ansible_collections/community/zabbix/plugins/modules/zabbix_templategroup.py index 7ab39d8fc..a7050f226 100644 --- a/ansible_collections/community/zabbix/plugins/modules/zabbix_templategroup.py +++ b/ansible_collections/community/zabbix/plugins/modules/zabbix_templategroup.py @@ -67,7 +67,7 @@ EXAMPLES = r''' ansible_httpapi_validate_certs: false ansible_zabbix_url_path: 'zabbixeu' # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu ansible_host: zabbix-example-fqdn.org - community.zabbix.zabbix_group: + community.zabbix.zabbix_templategroup: state: present template_groups: - Example group1 @@ -84,7 +84,7 @@ EXAMPLES = r''' ansible_httpapi_validate_certs: false ansible_zabbix_url_path: 'zabbixeu' # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu ansible_host: zabbix-example-fqdn.org - community.zabbix.zabbix_group: + community.zabbix.zabbix_templategroup: state: present template_groups: - Example group1 |