Files
netbox-zabbix-sync/tests/test_hostgroups.py
2026-02-19 12:08:01 +00:00

464 lines
19 KiB
Python

"""Tests for the Hostgroup class in the hostgroups module."""
import unittest
from unittest.mock import MagicMock, patch
from netbox_zabbix_sync.modules.exceptions import HostgroupError
from netbox_zabbix_sync.modules.hostgroups import Hostgroup
class TestHostgroups(unittest.TestCase):
"""Test class for Hostgroup functionality."""
def setUp(self):
"""Set up test fixtures."""
# Create mock logger
self.mock_logger = MagicMock()
# *** Mock NetBox Device setup ***
# Create mock device with all properties
self.mock_device = MagicMock()
self.mock_device.name = "test-device"
# Set up site information
site = MagicMock()
site.name = "TestSite"
# Set up region information
region = MagicMock()
region.name = "TestRegion"
# Ensure region string representation returns the name
region.__str__.return_value = "TestRegion"
site.region = region
# Set up site group information
site_group = MagicMock()
site_group.name = "TestSiteGroup"
# Ensure site group string representation returns the name
site_group.__str__.return_value = "TestSiteGroup"
site.group = site_group
self.mock_device.site = site
# Set up role information (varies based on NetBox version)
self.mock_device_role = MagicMock()
self.mock_device_role.name = "TestRole"
# Ensure string representation returns the name
self.mock_device_role.__str__.return_value = "TestRole"
self.mock_device.device_role = self.mock_device_role
self.mock_device.role = self.mock_device_role
# Set up tenant information
tenant = MagicMock()
tenant.name = "TestTenant"
# Ensure tenant string representation returns the name
tenant.__str__.return_value = "TestTenant"
tenant_group = MagicMock()
tenant_group.name = "TestTenantGroup"
# Ensure tenant group string representation returns the name
tenant_group.__str__.return_value = "TestTenantGroup"
tenant.group = tenant_group
self.mock_device.tenant = tenant
# Set up platform information
platform = MagicMock()
platform.name = "TestPlatform"
self.mock_device.platform = platform
# Device-specific properties
device_type = MagicMock()
manufacturer = MagicMock()
manufacturer.name = "TestManufacturer"
device_type.manufacturer = manufacturer
self.mock_device.device_type = device_type
location = MagicMock()
location.name = "TestLocation"
# Ensure location string representation returns the name
location.__str__.return_value = "TestLocation"
self.mock_device.location = location
rack = MagicMock()
rack.name = "TestRack"
self.mock_device.rack = rack
# Custom fields — empty_cf is intentionally None to test the empty CF path
self.mock_device.custom_fields = {"test_cf": "TestCF", "empty_cf": None}
# *** Mock NetBox VM setup ***
# Create mock VM with all properties
self.mock_vm = MagicMock()
self.mock_vm.name = "test-vm"
# Reuse site from device
self.mock_vm.site = site
# Set up role for VM
self.mock_vm.role = self.mock_device_role
# Set up tenant for VM (same as device)
self.mock_vm.tenant = tenant
# Set up platform for VM (same as device)
self.mock_vm.platform = platform
# VM-specific properties
cluster = MagicMock()
cluster.name = "TestCluster"
cluster_type = MagicMock()
cluster_type.name = "TestClusterType"
cluster.type = cluster_type
self.mock_vm.cluster = cluster
# Custom fields
self.mock_vm.custom_fields = {"test_cf": "TestCF"}
# Mock data for nesting tests
self.mock_regions_data = [
{"name": "ParentRegion", "parent": None, "_depth": 0},
{"name": "TestRegion", "parent": "ParentRegion", "_depth": 1},
]
self.mock_groups_data = [
{"name": "ParentSiteGroup", "parent": None, "_depth": 0},
{"name": "TestSiteGroup", "parent": "ParentSiteGroup", "_depth": 1},
]
def test_device_hostgroup_creation(self):
"""Test basic device hostgroup creation."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Test the string representation
self.assertEqual(str(hostgroup), "Hostgroup for dev test-device")
# Check format options were set correctly
self.assertEqual(hostgroup.format_options["site"], "TestSite")
self.assertEqual(hostgroup.format_options["region"], "TestRegion")
self.assertEqual(hostgroup.format_options["site_group"], "TestSiteGroup")
self.assertEqual(hostgroup.format_options["role"], "TestRole")
self.assertEqual(hostgroup.format_options["tenant"], "TestTenant")
self.assertEqual(hostgroup.format_options["tenant_group"], "TestTenantGroup")
self.assertEqual(hostgroup.format_options["platform"], "TestPlatform")
self.assertEqual(hostgroup.format_options["manufacturer"], "TestManufacturer")
self.assertEqual(hostgroup.format_options["location"], "TestLocation")
self.assertEqual(hostgroup.format_options["rack"], "TestRack")
def test_vm_hostgroup_creation(self):
"""Test basic VM hostgroup creation."""
hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger)
# Test the string representation
self.assertEqual(str(hostgroup), "Hostgroup for vm test-vm")
# Check format options were set correctly
self.assertEqual(hostgroup.format_options["site"], "TestSite")
self.assertEqual(hostgroup.format_options["region"], "TestRegion")
self.assertEqual(hostgroup.format_options["site_group"], "TestSiteGroup")
self.assertEqual(hostgroup.format_options["role"], "TestRole")
self.assertEqual(hostgroup.format_options["tenant"], "TestTenant")
self.assertEqual(hostgroup.format_options["tenant_group"], "TestTenantGroup")
self.assertEqual(hostgroup.format_options["platform"], "TestPlatform")
self.assertEqual(hostgroup.format_options["cluster"], "TestCluster")
self.assertEqual(hostgroup.format_options["cluster_type"], "TestClusterType")
def test_invalid_object_type(self):
"""Test that an invalid object type raises an exception."""
with self.assertRaises(HostgroupError):
Hostgroup("invalid", self.mock_device, "4.0", self.mock_logger)
def test_device_hostgroup_formats(self):
"""Test different hostgroup formats for devices."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Custom format: site/region
custom_result = hostgroup.generate("site/region")
self.assertEqual(custom_result, "TestSite/TestRegion")
# Custom format: site/tenant/platform/location
complex_result = hostgroup.generate("site/tenant/platform/location")
self.assertEqual(
complex_result, "TestSite/TestTenant/TestPlatform/TestLocation"
)
def test_vm_hostgroup_formats(self):
"""Test different hostgroup formats for VMs."""
hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger)
# Default format: cluster/role
default_result = hostgroup.generate("cluster/role")
self.assertEqual(default_result, "TestCluster/TestRole")
# Custom format: site/tenant
custom_result = hostgroup.generate("site/tenant")
self.assertEqual(custom_result, "TestSite/TestTenant")
# Custom format: cluster/cluster_type/platform
complex_result = hostgroup.generate("cluster/cluster_type/platform")
self.assertEqual(complex_result, "TestCluster/TestClusterType/TestPlatform")
def test_device_netbox_version_differences(self):
"""Test hostgroup generation with different NetBox versions.
device_role (v2/v3) and role (v4+) are set to different values so the
test can verify that the correct attribute is read for each version.
"""
# Build a device with deliberately different names on each role attribute
versioned_device = MagicMock()
versioned_device.name = "versioned-device"
versioned_device.site = self.mock_device.site
versioned_device.tenant = self.mock_device.tenant
versioned_device.platform = self.mock_device.platform
versioned_device.location = self.mock_device.location
versioned_device.rack = self.mock_device.rack
versioned_device.device_type = self.mock_device.device_type
versioned_device.custom_fields = self.mock_device.custom_fields
old_role = MagicMock()
old_role.name = "OldRole"
new_role = MagicMock()
new_role.name = "NewRole"
versioned_device.device_role = old_role # read by NetBox v2 / v3 code path
versioned_device.role = new_role # read by NetBox v4+ code path
# v2 must use device_role
hostgroup_v2 = Hostgroup("dev", versioned_device, "2.11", self.mock_logger)
self.assertEqual(hostgroup_v2.format_options["role"], "OldRole")
# v3 must also use device_role
hostgroup_v3 = Hostgroup("dev", versioned_device, "3.5", self.mock_logger)
self.assertEqual(hostgroup_v3.format_options["role"], "OldRole")
# v4+ must use role
hostgroup_v4 = Hostgroup("dev", versioned_device, "4.0", self.mock_logger)
self.assertEqual(hostgroup_v4.format_options["role"], "NewRole")
def test_custom_field_lookup(self):
"""Test custom field lookup functionality."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Test custom field exists and is populated
cf_result = hostgroup.custom_field_lookup("test_cf")
self.assertTrue(cf_result["result"])
self.assertEqual(cf_result["cf"], "TestCF")
# Test custom field doesn't exist
cf_result = hostgroup.custom_field_lookup("nonexistent_cf")
self.assertFalse(cf_result["result"])
self.assertIsNone(cf_result["cf"])
# Test custom field exists but has no value (None)
cf_result = hostgroup.custom_field_lookup("empty_cf")
self.assertTrue(cf_result["result"]) # key is present
self.assertIsNone(cf_result["cf"]) # value is empty
def test_hostgroup_with_custom_field(self):
"""Test hostgroup generation including a custom field."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Generate with custom field included
result = hostgroup.generate("site/test_cf/role")
self.assertEqual(result, "TestSite/TestCF/TestRole")
def test_missing_hostgroup_format_item(self):
"""Test handling of missing hostgroup format items."""
# Create a device with minimal attributes
minimal_device = MagicMock()
minimal_device.name = "minimal-device"
minimal_device.site = None
minimal_device.tenant = None
minimal_device.platform = None
minimal_device.custom_fields = {}
# Create role
role = MagicMock()
role.name = "MinimalRole"
minimal_device.role = role
# Create device_type with manufacturer
device_type = MagicMock()
manufacturer = MagicMock()
manufacturer.name = "MinimalManufacturer"
device_type.manufacturer = manufacturer
minimal_device.device_type = device_type
# Create hostgroup
hostgroup = Hostgroup("dev", minimal_device, "4.0", self.mock_logger)
# Generate with default format
result = hostgroup.generate("site/manufacturer/role")
# Site is missing, so only manufacturer and role should be included
self.assertEqual(result, "MinimalManufacturer/MinimalRole")
# Test with invalid format
with self.assertRaises(HostgroupError):
hostgroup.generate("site/nonexistent/role")
def test_nested_region_hostgroups(self):
"""Test hostgroup generation with nested regions."""
# Mock the build_path function to return a predictable result
with patch(
"netbox_zabbix_sync.modules.hostgroups.build_path"
) as mock_build_path:
# Configure the mock to return a list of regions in the path
mock_build_path.return_value = ["ParentRegion", "TestRegion"]
# Create hostgroup with nested regions enabled
hostgroup = Hostgroup(
"dev",
self.mock_device,
"4.0",
self.mock_logger,
nested_region_flag=True,
nb_regions=self.mock_regions_data,
)
# Generate hostgroup with region
result = hostgroup.generate("site/region/role")
# Should include the parent region
self.assertEqual(result, "TestSite/ParentRegion/TestRegion/TestRole")
def test_nested_sitegroup_hostgroups(self):
"""Test hostgroup generation with nested site groups."""
# Mock the build_path function to return a predictable result
with patch(
"netbox_zabbix_sync.modules.hostgroups.build_path"
) as mock_build_path:
# Configure the mock to return a list of site groups in the path
mock_build_path.return_value = ["ParentSiteGroup", "TestSiteGroup"]
# Create hostgroup with nested site groups enabled
hostgroup = Hostgroup(
"dev",
self.mock_device,
"4.0",
self.mock_logger,
nested_sitegroup_flag=True,
nb_groups=self.mock_groups_data,
)
# Generate hostgroup with site_group
result = hostgroup.generate("site/site_group/role")
# Should include the parent site group
self.assertEqual(result, "TestSite/ParentSiteGroup/TestSiteGroup/TestRole")
def test_vm_list_based_hostgroup_format(self):
"""Test VM hostgroup generation with a list-based format."""
hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger)
# Test with a list of format strings
format_list = ["platform", "role", "cluster_type/cluster"]
# Generate hostgroups for each format in the list
hostgroups = []
for fmt in format_list:
result = hostgroup.generate(fmt)
if result: # Only add non-None results
hostgroups.append(result)
# Verify each expected hostgroup is generated
self.assertEqual(len(hostgroups), 3) # Should have 3 hostgroups
self.assertIn("TestPlatform", hostgroups)
self.assertIn("TestRole", hostgroups)
self.assertIn("TestClusterType/TestCluster", hostgroups)
def test_nested_format_splitting(self):
"""Test that formats with slashes correctly split and resolve each component."""
hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger)
# Test a format with slashes that should be split
complex_format = "cluster_type/cluster"
result = hostgroup.generate(complex_format)
# Verify the format is correctly split and each component resolved
self.assertEqual(result, "TestClusterType/TestCluster")
def test_multiple_hostgroup_formats_device(self):
"""Test device hostgroup generation with multiple formats."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Test with various formats that would be in a list
formats = [
"site",
"manufacturer/role",
"platform/location",
"tenant_group/tenant",
]
# Generate and check each format
results = {}
for fmt in formats:
results[fmt] = hostgroup.generate(fmt)
# Verify results
self.assertEqual(results["site"], "TestSite")
self.assertEqual(results["manufacturer/role"], "TestManufacturer/TestRole")
self.assertEqual(results["platform/location"], "TestPlatform/TestLocation")
self.assertEqual(results["tenant_group/tenant"], "TestTenantGroup/TestTenant")
def test_literal_string_in_format(self):
"""Test that quoted literal strings in a format are used verbatim."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# Single-quoted literal
result = hostgroup.generate("'MyDevices'/role")
self.assertEqual(result, "MyDevices/TestRole")
# Double-quoted literal
result = hostgroup.generate('"MyDevices"/role')
self.assertEqual(result, "MyDevices/TestRole")
def test_generate_returns_none_when_all_fields_empty(self):
"""Test that generate() returns None when every format field resolves to no value."""
empty_device = MagicMock()
empty_device.name = "empty-device"
empty_device.site = None
empty_device.tenant = None
empty_device.platform = None
empty_device.role = None
empty_device.location = None
empty_device.rack = None
empty_device.custom_fields = {}
device_type = MagicMock()
manufacturer = MagicMock()
manufacturer.name = "SomeManufacturer"
device_type.manufacturer = manufacturer
empty_device.device_type = device_type
hostgroup = Hostgroup("dev", empty_device, "4.0", self.mock_logger)
# site, tenant and platform all have no value → hg_output stays empty → None
result = hostgroup.generate("site/tenant/platform")
self.assertIsNone(result)
def test_vm_without_cluster(self):
"""Test that cluster/cluster_type are absent from format_options when VM has no cluster."""
clusterless_vm = MagicMock()
clusterless_vm.name = "clusterless-vm"
clusterless_vm.site = self.mock_vm.site
clusterless_vm.tenant = self.mock_vm.tenant
clusterless_vm.platform = self.mock_vm.platform
clusterless_vm.role = self.mock_device_role
clusterless_vm.cluster = None
clusterless_vm.custom_fields = {}
hostgroup = Hostgroup("vm", clusterless_vm, "4.0", self.mock_logger)
# cluster and cluster_type must not appear in format_options
self.assertNotIn("cluster", hostgroup.format_options)
self.assertNotIn("cluster_type", hostgroup.format_options)
# Requesting cluster in a format must raise HostgroupError
with self.assertRaises(HostgroupError):
hostgroup.generate("cluster/role")
def test_empty_custom_field_skipped_in_format(self):
"""Test that an empty (None) custom field is silently omitted from the hostgroup name."""
hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger)
# empty_cf has no value → it is skipped; only site and role appear
result = hostgroup.generate("site/empty_cf/role")
self.assertEqual(result, "TestSite/TestRole")
if __name__ == "__main__":
unittest.main()