mirror of
https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
synced 2026-03-21 12:08:39 -06:00
Added #112
This commit is contained in:
@@ -12,6 +12,15 @@ templates_config_context_overrule = False
|
||||
template_cf = "zabbix_template"
|
||||
device_cf = "zabbix_hostid"
|
||||
|
||||
# Zabbix host description
|
||||
# The following options are available for the description of all created hosts in Zabbix
|
||||
# static: Uses the default static string "Host added by NetBox sync script."
|
||||
# dynamic: "Uses a predefined dynamic string which resolves the owner of an object and datetime. Recommended for users who use Netbox 4.5+
|
||||
# custom: Use a custom string such as "This host was created by Zabbix-sync on machine MGMT01.internal". It is also posible to resolve dynamic values in this string using {} markers.
|
||||
description = "static"
|
||||
# The timedate format which is used for generating the datetime macro when used in the dynamic description type or custom type.
|
||||
description_dt_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
## Enable clustering of devices with virtual chassis setup
|
||||
clustering = False
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from netbox_zabbix_sync.modules.exceptions import (
|
||||
SyncInventoryError,
|
||||
TemplateError,
|
||||
)
|
||||
from netbox_zabbix_sync.modules.host_description import Description
|
||||
from netbox_zabbix_sync.modules.hostgroups import Hostgroup
|
||||
from netbox_zabbix_sync.modules.interface import ZabbixInterface
|
||||
from netbox_zabbix_sync.modules.settings import load_config
|
||||
@@ -534,13 +535,7 @@ class PhysicalDevice:
|
||||
)
|
||||
return False
|
||||
|
||||
def create_in_zabbix(
|
||||
self,
|
||||
groups,
|
||||
templates,
|
||||
proxies,
|
||||
description="Host added by NetBox sync script.",
|
||||
):
|
||||
def create_in_zabbix(self, groups, templates, proxies):
|
||||
"""
|
||||
Creates Zabbix host object with parameters from NetBox object.
|
||||
"""
|
||||
@@ -562,6 +557,14 @@ class PhysicalDevice:
|
||||
interfaces = self.set_interface_details()
|
||||
# Set Zabbix proxy if defined
|
||||
self._set_proxy(proxies)
|
||||
# Set description
|
||||
description_handler = Description(
|
||||
self.nb,
|
||||
self.config,
|
||||
logger=self.logger,
|
||||
nb_version=self.nb_api_version,
|
||||
)
|
||||
description = description_handler.generate()
|
||||
# Set basic data for host creation
|
||||
create_data = {
|
||||
"host": self.name,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Modules that set description of a host in Zabbix
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
from re import findall as re_findall
|
||||
|
||||
|
||||
class Description:
|
||||
"""
|
||||
Class that generates the description for a host in Zabbix based on the configuration provided.
|
||||
|
||||
INPUT:
|
||||
- netbox_object: The NetBox object that is being synced.
|
||||
- configuration: configuration of the syncer.
|
||||
Required keys in configuration:
|
||||
description: Can be "static", "dynamic" or a custom description with macros.
|
||||
- nb_version: The version of NetBox that is being used.
|
||||
"""
|
||||
|
||||
def __init__(self, netbox_object, configuration, nb_version, logger=None):
|
||||
self.netbox_object = netbox_object
|
||||
self.name = self.netbox_object.name
|
||||
self.configuration = configuration
|
||||
self.nb_version = nb_version
|
||||
self.logger = logger or getLogger(__name__)
|
||||
self._set_default_macro_values()
|
||||
self._set_defaults()
|
||||
|
||||
def _set_default_macro_values(self):
|
||||
"""
|
||||
Sets the default macro values for the description.
|
||||
"""
|
||||
# Get the datetime format from the configuration,
|
||||
# or use the default format if not provided
|
||||
dt_format = self.configuration.get("description_dt_format", "%Y-%m-%d %H:%M:%S")
|
||||
# Set the datetime macro
|
||||
try:
|
||||
datetime_value = datetime.now().strftime(dt_format)
|
||||
except (ValueError, TypeError) as e:
|
||||
self.logger.warning(
|
||||
"Host %s: invalid datetime format '%s': %s. Using default format.",
|
||||
self.name,
|
||||
dt_format,
|
||||
e,
|
||||
)
|
||||
datetime_value = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Set the owner macro
|
||||
owner = self.netbox_object.owner if self.nb_version >= "4.5" else ""
|
||||
# Set the macro list
|
||||
self.macros = {"{datetime}": datetime_value, "{owner}": owner}
|
||||
|
||||
def _resolve_macros(self, description):
|
||||
"""
|
||||
Takes a description and resolves the macro's in it.
|
||||
Returns the description with the macro's resolved.
|
||||
"""
|
||||
# Find all macros in the description
|
||||
provided_macros = re_findall(r"\{\w+\}", description)
|
||||
# Go through all macros provided in the NB description
|
||||
for macro in provided_macros:
|
||||
# If the macro is in the list of default macro values
|
||||
if macro in self.macros:
|
||||
# Replace the macro in the description with the value of the macro
|
||||
description = description.replace(macro, str(self.macros[macro]))
|
||||
else:
|
||||
# One of the macro's is invalid.
|
||||
self.logger.warning(
|
||||
"Host %s: macro %s is not valid. Failing back to default.",
|
||||
self.name,
|
||||
macro,
|
||||
)
|
||||
return False
|
||||
return description
|
||||
|
||||
def _set_defaults(self):
|
||||
"""
|
||||
Sets the default descriptions for the host.
|
||||
"""
|
||||
self.defaults = {
|
||||
"static": "Host added by NetBox sync script.",
|
||||
"dynamic": (
|
||||
"Host by owner {owner} added by NetBox sync script on {datetime}."
|
||||
),
|
||||
}
|
||||
|
||||
def _custom_override(self):
|
||||
"""
|
||||
Checks if the description is mentioned in the config context.
|
||||
"""
|
||||
zabbix_config = self.netbox_object.config_context.get("zabbix")
|
||||
if zabbix_config and "description" in zabbix_config:
|
||||
return zabbix_config["description"]
|
||||
return False
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Generates the description for the host.
|
||||
"""
|
||||
# First: check if an override is present.
|
||||
config_context_description = self._custom_override()
|
||||
if config_context_description is not False:
|
||||
resolved = self._resolve_macros(config_context_description)
|
||||
return resolved if resolved else self.defaults["static"]
|
||||
# Override is not present: continue with config description
|
||||
description = ""
|
||||
if "description" not in self.configuration:
|
||||
# If no description config is provided, use default static
|
||||
return self.defaults["static"]
|
||||
if not self.configuration["description"]:
|
||||
# The configuration is set to False, meaning an empty description
|
||||
return description
|
||||
if self.configuration["description"] in self.defaults:
|
||||
# The description is one of the default options
|
||||
description = self.defaults[self.configuration["description"]]
|
||||
else:
|
||||
# The description is set to a custom description
|
||||
description = self.configuration["description"]
|
||||
# Resolve the macro's in the description
|
||||
final_description = self._resolve_macros(description)
|
||||
if final_description:
|
||||
return final_description
|
||||
return self.defaults["static"]
|
||||
@@ -82,6 +82,8 @@ DEFAULT_CONFIG = {
|
||||
"cluster/name": "cluster",
|
||||
"platform/name": "target",
|
||||
},
|
||||
"description_dt_format": "%Y-%m-%d %H:%M:%S",
|
||||
"description": "static",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Tests for the Description class in the host_description module."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from netbox_zabbix_sync.modules.host_description import Description
|
||||
|
||||
|
||||
class TestDescription(unittest.TestCase):
|
||||
"""Test class for Description functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create mock NetBox object
|
||||
self.mock_nb_object = MagicMock()
|
||||
self.mock_nb_object.name = "test-host"
|
||||
self.mock_nb_object.owner = "admin"
|
||||
self.mock_nb_object.config_context = {}
|
||||
|
||||
# Create logger mock
|
||||
self.mock_logger = MagicMock()
|
||||
|
||||
# Base configuration
|
||||
self.base_config = {}
|
||||
|
||||
# Test 1: Config context description override
|
||||
@patch("netbox_zabbix_sync.modules.host_description.datetime")
|
||||
def test_1_config_context_override_value(self, mock_datetime):
|
||||
"""Test 1: User that provides a config context description value should get this override value back."""
|
||||
mock_now = MagicMock()
|
||||
mock_now.strftime.return_value = "2026-02-25 10:30:00"
|
||||
mock_datetime.now.return_value = mock_now
|
||||
|
||||
# Set config context with description
|
||||
self.mock_nb_object.config_context = {
|
||||
"zabbix": {"description": "Custom override for {owner}"}
|
||||
}
|
||||
|
||||
config = {"description": "static"}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
# Should use config context, not config
|
||||
self.assertEqual(result, "Custom override for admin")
|
||||
|
||||
# Test 2: Static description
|
||||
def test_2_static_description(
|
||||
self,
|
||||
):
|
||||
"""Test 2: User that provides static as description should get the default static value."""
|
||||
config = {"description": "static"}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
self.assertEqual(result, "Host added by NetBox sync script.")
|
||||
|
||||
# Test 3: Dynamic description
|
||||
@patch("netbox_zabbix_sync.modules.host_description.datetime")
|
||||
def test_3_dynamic_description(self, mock_datetime):
|
||||
"""Test 3: User that provides 'dynamic' should get the resolved description string back."""
|
||||
mock_now = MagicMock()
|
||||
mock_now.strftime.return_value = "2026-02-25 10:30:00"
|
||||
mock_datetime.now.return_value = mock_now
|
||||
|
||||
config = {"description": "dynamic"}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
expected = (
|
||||
"Host by owner admin added by NetBox sync script on 2026-02-25 10:30:00."
|
||||
)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# Test 4: Invalid macro fallback
|
||||
def test_4_invalid_macro_fallback_to_static(self):
|
||||
"""Test 4: Users who provide invalid macros should fallback to the static variant."""
|
||||
config = {"description": "Host {owner} with {invalid_macro}"}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
# Should fall back to static default
|
||||
self.assertEqual(result, "Host added by NetBox sync script.")
|
||||
# Verify warning was logged
|
||||
self.mock_logger.warning.assert_called_once()
|
||||
|
||||
# Test 5: Custom time format
|
||||
@patch("netbox_zabbix_sync.modules.host_description.datetime")
|
||||
def test_5_custom_datetime_format(self, mock_datetime):
|
||||
"""Test 5: Users who change the time format."""
|
||||
mock_now = MagicMock()
|
||||
# Will be called twice: once with custom format, once for string
|
||||
mock_now.strftime.side_effect = ["25/02/2026", "25/02/2026"]
|
||||
mock_datetime.now.return_value = mock_now
|
||||
|
||||
config = {
|
||||
"description": "Updated on {datetime}",
|
||||
"description_dt_format": "%d/%m/%Y",
|
||||
}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
self.assertEqual(result, "Updated on 25/02/2026")
|
||||
|
||||
# Test 6: Custom description format in config
|
||||
@patch("netbox_zabbix_sync.modules.host_description.datetime")
|
||||
def test_6_custom_description_format(self, mock_datetime):
|
||||
"""Test 6: Users who provide a custom description format in the config."""
|
||||
mock_now = MagicMock()
|
||||
mock_now.strftime.return_value = "2026-02-25 10:30:00"
|
||||
mock_datetime.now.return_value = mock_now
|
||||
|
||||
config = {"description": "Server {owner} managed at {datetime}"}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
self.assertEqual(result, "Server admin managed at 2026-02-25 10:30:00")
|
||||
|
||||
# Test 7: Owner on lower NetBox version
|
||||
@patch("netbox_zabbix_sync.modules.host_description.datetime")
|
||||
def test_7_owner_on_lower_netbox_version(self, mock_datetime):
|
||||
"""Test 7: Users who try to resolve the owner property on a lower NetBox version (3.2)."""
|
||||
mock_now = MagicMock()
|
||||
mock_now.strftime.return_value = "2026-02-25 10:30:00"
|
||||
mock_datetime.now.return_value = mock_now
|
||||
|
||||
config = {"description": "Device owned by {owner}"}
|
||||
desc = Description(
|
||||
self.mock_nb_object,
|
||||
config,
|
||||
"3.2", # Lower NetBox version
|
||||
logger=self.mock_logger,
|
||||
)
|
||||
|
||||
result = desc.generate()
|
||||
# Owner should be empty string on version < 4.5
|
||||
self.assertEqual(result, "Device owned by ")
|
||||
|
||||
# Test 8: Missing or False description returns static
|
||||
def test_8a_missing_description_returns_static(self):
|
||||
"""Test 8a: When description option is not found, script should return the static variant."""
|
||||
config = {} # No description key
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
self.assertEqual(result, "Host added by NetBox sync script.")
|
||||
|
||||
def test_8b_false_description_returns_empty(self):
|
||||
"""Test 8b: When description is set to False, script should return empty string."""
|
||||
config = {"description": False}
|
||||
desc = Description(self.mock_nb_object, config, "4.5", logger=self.mock_logger)
|
||||
|
||||
result = desc.generate()
|
||||
self.assertEqual(result, "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user