diff --git a/README.md b/README.md index 78ab8bd..d839266 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # About -Zabbix-auto-config is an utility that aims to automatically configure hosts, host groups, host inventories and templates in the monitoring software [Zabbix](https://www.zabbix.com/). +Zabbix-auto-config is an utility that aims to automatically configure hosts, host groups, host inventories, template groups and templates in the monitoring software [Zabbix](https://www.zabbix.com/). -Note: This is only tested with Zabbix 5.0 LTS. +Note: Only tested with Zabbix 6.0 and 6.4. ## Requirements @@ -101,7 +101,7 @@ def collect(*args: Any, **kwargs: Any) -> List[Host]: if __name__ == "__main__": for host in collect(): - print(host.json()) + print(host.model_dump_json()) EOF cat > path/to/host_modifier_dir/mod.py << EOF from zabbix_auto_config.models import Host diff --git a/config.sample.toml b/config.sample.toml index 538f5a3..aa69f42 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -49,12 +49,18 @@ tags_prefix = "zac_" managed_inventory = ["location"] # Names of hostgroups that zabbix-auto-config will manage. -#hostgroup_all = "All-hosts" -#hostgroup_manual = "All-manual-hosts" -#hostgroup_disabled = "All-auto-disabled-hosts" -#hostgroup_source_prefix = "Source-" -#hostgroup_importance_prefix = "Importance-" -#extra_siteadmin_hostgroup_prefixes = [] +hostgroup_all = "All-hosts" +hostgroup_manual = "All-manual-hosts" +hostgroup_disabled = "All-auto-disabled-hosts" +hostgroup_source_prefix = "Source-" +hostgroup_importance_prefix = "Importance-" + +# Template group creation +# NOTE: will create host groups if enabled on Zabbix <6.2 +create_templategroups = true +templategroup_prefix = "Templates-" + +extra_siteadmin_hostgroup_prefixes = [] [source_collectors.mysource] # Name of the source collector module without the .py extension @@ -87,5 +93,5 @@ another_kwarg = "value2" # We can pass an arbitrary number of kwargs to module_name = "mysource" update_interval = 60 error_tolerance = 0 # no tolerance for errors (default) -exit_on_error = true # exit application if source fails +exit_on_error = true # exit application if source fails (default) source = "other" # extra kwarg used in mysource module diff --git a/pyproject.toml b/pyproject.toml index fd46e51..9879390 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "pyzabbix>=1.3.0", "requests>=1.0.0", "tomli>=2.0.0", + "packaging>=23.2", ] [project.optional-dependencies] diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..ce71eb5 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,40 @@ +"""Santiy testing of Zabbix API version parsing. + +Tests against known versions of Zabbix. Expects support for alpha, beta and rc. +""" + +from typing import Tuple +from packaging.version import Version +import pytest + + +@pytest.mark.parametrize( + "version, release", + [ + # Certain major versions released in 2023 + ("7.0.0", (7, 0, 0)), + ("6.4.8", (6, 4, 8)), + ("6.0.23", (6, 0, 23)), + ("5.0.39", (5, 0, 39)), + ("6.2.9", (6, 2, 9)), + # Pre-release versions + ("7.0.0alpha7", (7, 0, 0)), + ("7.0.0a7", (7, 0, 0)), # short form + ("6.4.0beta6", (6, 4, 0)), + ("6.4.0b6", (6, 4, 0)), # short form + ("6.4.8rc1", (6, 4, 8)), + ], +) +def test_version(version: str, release: Tuple[int, int, int]): + """Test that the version string is parsed correctly.""" + v = Version(version) + assert v.release == release + assert v.major == release[0] + assert v.minor == release[1] + assert v.micro == release[2] + + # Test comparison + assert v.release < (999, 999, 999) + assert v.release > (0, 0, 0) + assert v > Version("0.0.0") + assert v < Version("999.999.999") diff --git a/zabbix_auto_config/models.py b/zabbix_auto_config/models.py index 4577a87..687011c 100644 --- a/zabbix_auto_config/models.py +++ b/zabbix_auto_config/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union @@ -62,13 +64,16 @@ class ZabbixSettings(ConfigBaseModel): hostgroup_source_prefix: str = "Source-" hostgroup_importance_prefix: str = "Importance-" + + create_templategroups: bool = False + templategroup_prefix: str = "Templates-" # Prefixes for extra host groups to create based on the host groups # in the siteadmin mapping. - # e.g. Siteadmin-foo -> Templates-foo if list is ["Templates-"] + # e.g. Siteadmin-foo -> Secondary-foo if list is ["Secondary-"] # The groups must have prefixes separated by a hyphen (-) in order # to replace them with any of these prefixes. - # These groups are not managed by ZAC beyond creating them. + # These groups are not managed by ZAC beyond their creation. extra_siteadmin_hostgroup_prefixes: Set[str] = set() @field_validator("timeout") diff --git a/zabbix_auto_config/processing.py b/zabbix_auto_config/processing.py index d5f46be..17abe46 100644 --- a/zabbix_auto_config/processing.py +++ b/zabbix_auto_config/processing.py @@ -13,8 +13,9 @@ import signal import itertools import queue -from typing import Dict, List, TYPE_CHECKING, Optional +from typing import Dict, List, TYPE_CHECKING, Optional, Set +from packaging.version import Version import psycopg2 from pydantic import ValidationError import pyzabbix @@ -604,6 +605,9 @@ def __init__(self, name, state, db_uri, settings: models.Settings): os.path.join(self.config.map_dir, "siteadmin_hostgroup_map.txt") ) + ver = self.api.apiinfo.version() + self.zabbix_version = Version(ver) + def work(self): start_time = time.time() logging.info("Zabbix update starting") @@ -983,8 +987,8 @@ def clear_templates(self, templates, host): logging.debug("DRYRUN: Clearing templates on host: '%s'", host["host"]) def set_templates(self, templates, host): - logging.debug("Setting templates on host: '%s'", host["host"]) if not self.config.dryrun: + logging.debug("Setting templates on host: '%s'", host["host"]) try: templates = [{"templateid": template_id} for _, template_id in templates.items()] self.api.host.update(hostid=host["hostid"], templates=templates) @@ -1064,24 +1068,29 @@ def set_hostgroups(self, hostgroups, host): else: logging.debug("DRYRUN: Setting hostgroups on host: '%s'", host["host"]) - def create_hostgroup(self, hostgroup_name): - if not self.config.dryrun: - logging.debug("Creating hostgroup: '%s'", hostgroup_name) - try: - result = self.api.hostgroup.create(name=hostgroup_name) - return result["groupids"][0] - except pyzabbix.ZabbixAPIException as e: - logging.error("Error when creating hostgroups '%s': %s", hostgroup_name, e.args) - else: + def create_hostgroup(self, hostgroup_name: str) -> Optional[str]: + if self.config.dryrun: logging.debug("DRYRUN: Creating hostgroup: '%s'", hostgroup_name) - return "-1" + return None + + logging.debug("Creating hostgroup: '%s'", hostgroup_name) + try: + result = self.api.hostgroup.create(name=hostgroup_name) + groupid = result["groupids"][0] + logging.info("Created host group '%s' (%s)", hostgroup_name, groupid) + return groupid + except pyzabbix.ZabbixAPIException as e: + logging.error( + "Error when creating hostgroups '%s': %s", hostgroup_name, e.args + ) + return None def create_extra_hostgroups( self, existing_hostgroups: List[Dict[str, str]] ) -> None: """Creates additonal host groups based on the prefixes specified in the config file. These host groups are not assigned hosts by ZAC.""" - hostgroup_names = [h["name"] for h in existing_hostgroups] + hostgroup_names = set(h["name"] for h in existing_hostgroups) for prefix in self.config.extra_siteadmin_hostgroup_prefixes: mapping = utils.mapping_values_with_prefix( @@ -1094,6 +1103,74 @@ def create_extra_hostgroups( continue self.create_hostgroup(hostgroup) + def create_templategroup(self, templategroup_name: str) -> Optional[str]: + if self.config.dryrun: + logging.debug("DRYRUN: Creating template group: '%s'", templategroup_name) + return None + + logging.debug("Creating template group: '%s'", templategroup_name) + try: + result = self.api.templategroup.create(name=templategroup_name) + groupid = result["groupids"][0] + logging.info("Created template group '%s' (%s)", templategroup_name, groupid) + return groupid + except pyzabbix.ZabbixAPIException as e: + logging.error( + "Error when creating template group '%s': %s", + templategroup_name, + e.args, + ) + return None + + def create_templategroups(self, existing_hostgroups: List[Dict[str, str]]) -> None: + """Creates template groups for each host group in the siteadmin + mapping file with the configured template group prefix. + + For Zabbix <6.2, host groups are created instead of template groups.""" + # Construct a set of all template group names from siteadmin mapping file + # by replacing the host group prefix with the template group prefix + tgroups = set( + utils.with_prefix(tg, self.config.templategroup_prefix) + for tg in itertools.chain.from_iterable( + self.siteadmin_hostgroup_map.values() + ) + ) + if self.zabbix_version.release >= (6, 2, 0): + logging.debug("Zabbix version is %s. Creating template groups.", self.zabbix_version) + self._create_templategroups(tgroups) + else: + logging.debug("Zabbix version is %s. Creating template groups as host groups.", self.zabbix_version) + self._create_templategroups_pre_62_compat(tgroups, existing_hostgroups) + + + def _create_templategroups(self, tgroups: Set[str]) -> None: + """Create the given template groups if they don't exist. + + Args: + tgroups: A set of template group names to create. + """ + res = self.api.templategroup.get(output=["name", "groupid"]) + existing_tgroups = set(tg["name"] for tg in res) + for tgroup in tgroups: + if tgroup in existing_tgroups: + continue + self.create_templategroup(tgroup) + + def _create_templategroups_pre_62_compat(self, tgroups: Set[str], existing_hostgroups: List[Dict[str, str]]) -> None: + """Compatibility method for creating template groups on Zabbix <6.2. + + Because template groups do not exist in <6.2, we instead create + host groups with the given names. + + Args: + tgroups: A set of template group names to create. + """ + existing_hgroup_names = set(h["name"] for h in existing_hostgroups) + for tgroup in tgroups: + if tgroup in existing_hgroup_names: + continue + self.create_hostgroup(tgroup) + def do_update(self): managed_hostgroup_names = set( itertools.chain.from_iterable(self.property_hostgroup_map.values()) @@ -1107,6 +1184,10 @@ def do_update(self): # Create extra host groups if necessary if self.config.extra_siteadmin_hostgroup_prefixes: self.create_extra_hostgroups(existing_hostgroups) + + # Create template groups if enabled + if self.config.create_templategroups: + self.create_templategroups(existing_hostgroups) zabbix_hostgroups = {} for zabbix_hostgroup in existing_hostgroups: