Merge branch 'develop' into additional-hostgroup-support

This commit is contained in:
Raymond Kuiper
2025-06-16 10:06:47 +02:00
committed by GitHub
22 changed files with 1551 additions and 212 deletions

View File

@@ -2,17 +2,17 @@
"""
Device specific handeling for NetBox to Zabbix
"""
from copy import deepcopy
from logging import getLogger
from os import sys
from re import search
from operator import itemgetter
from zabbix_utils import APIRequestError
from pynetbox import RequestError as NetboxRequestError
from modules.exceptions import (
InterfaceConfigError,
JournalError,
SyncExternalError,
SyncInventoryError,
TemplateError,
@@ -20,33 +20,11 @@ from modules.exceptions import (
from modules.hostgroups import Hostgroup
from modules.interface import ZabbixInterface
from modules.tags import ZabbixTags
from modules.tools import field_mapper, remove_duplicates
from modules.tools import field_mapper, remove_duplicates, sanatize_log_output
from modules.usermacros import ZabbixUsermacros
from modules.config import load_config
try:
from config import (
device_cf,
device_inventory_map,
device_tag_map,
device_usermacro_map,
inventory_mode,
inventory_sync,
tag_lower,
tag_name,
tag_sync,
tag_value,
template_cf,
traverse_regions,
traverse_site_groups,
usermacro_sync,
)
except ModuleNotFoundError:
print(
"Configuration file config.py not found in main directory."
"Please create the file or rename the config.py.example file to config.py."
)
sys.exit(0)
config = load_config()
class PhysicalDevice:
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments
@@ -78,7 +56,7 @@ class PhysicalDevice:
self.nb_journals = nb_journal_class
self.inventory_mode = -1
self.inventory = {}
self.usermacros = {}
self.usermacros = []
self.tags = {}
self.logger = logger if logger else getLogger(__name__)
self._setBasics()
@@ -91,15 +69,15 @@ class PhysicalDevice:
def _inventory_map(self):
"""Use device inventory maps"""
return device_inventory_map
return config["device_inventory_map"]
def _usermacro_map(self):
"""Use device inventory maps"""
return device_usermacro_map
return config["device_usermacro_map"]
def _tag_map(self):
"""Use device host tag maps"""
return device_tag_map
return config["device_tag_map"]
def _setBasics(self):
"""
@@ -115,10 +93,10 @@ class PhysicalDevice:
raise SyncInventoryError(e)
# Check if device has custom field for ZBX ID
if device_cf in self.nb.custom_fields:
self.zabbix_id = self.nb.custom_fields[device_cf]
if config["device_cf"] in self.nb.custom_fields:
self.zabbix_id = self.nb.custom_fields[config["device_cf"]]
else:
e = f"Host {self.name}: Custom field {device_cf} not present"
e = f'Host {self.name}: Custom field {config["device_cf"]} not present'
self.logger.warning(e)
raise SyncInventoryError(e)
@@ -147,8 +125,8 @@ class PhysicalDevice:
self.nb,
self.nb_api_version,
logger=self.logger,
nested_sitegroup_flag=traverse_site_groups,
nested_region_flag=traverse_regions,
nested_sitegroup_flag=config['traverse_site_groups'],
nested_region_flag=config['traverse_regions'],
nb_groups=nb_site_groups,
nb_regions=nb_regions,
)
@@ -187,18 +165,20 @@ class PhysicalDevice:
# Get Zabbix templates from the device type
device_type_cfs = self.nb.device_type.custom_fields
# Check if the ZBX Template CF is present
if template_cf in device_type_cfs:
if config["template_cf"] in device_type_cfs:
# Set value to template
return [device_type_cfs[template_cf]]
return [device_type_cfs[config["template_cf"]]]
# Custom field not found, return error
e = (
f"Custom field {template_cf} not "
f"Custom field {config['template_cf']} not "
f"found for {self.nb.device_type.manufacturer.name}"
f" - {self.nb.device_type.display}."
)
self.logger.warning(e)
raise TemplateError(e)
def get_templates_context(self):
"""Get Zabbix templates from the device context"""
if "zabbix" not in self.config_context:
@@ -221,25 +201,24 @@ class PhysicalDevice:
def set_inventory(self, nbdevice):
"""Set host inventory"""
# Set inventory mode. Default is disabled (see class init function).
if inventory_mode == "disabled":
if inventory_sync:
self.logger.error(
f"Host {self.name}: Unable to map NetBox inventory to Zabbix. "
"Inventory sync is enabled in config but inventory mode is disabled."
)
if config["inventory_mode"] == "disabled":
if config["inventory_sync"]:
self.logger.error(f"Host {self.name}: Unable to map NetBox inventory to Zabbix. "
"Inventory sync is enabled in "
"config but inventory mode is disabled.")
return True
if inventory_mode == "manual":
if config["inventory_mode"] == "manual":
self.inventory_mode = 0
elif inventory_mode == "automatic":
elif config["inventory_mode"] == "automatic":
self.inventory_mode = 1
else:
self.logger.error(
f"Host {self.name}: Specified value for inventory mode in"
f" config is not valid. Got value {inventory_mode}"
f" config is not valid. Got value {config['inventory_mode']}"
)
return False
self.inventory = {}
if inventory_sync and self.inventory_mode in [0, 1]:
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
self.logger.debug(f"Host {self.name}: Starting inventory mapper")
self.inventory = field_mapper(
self.name, self._inventory_map(), nbdevice, self.logger
@@ -377,7 +356,7 @@ class PhysicalDevice:
def _zeroize_cf(self):
"""Sets the hostID custom field in NetBox to zero,
effectively destroying the link"""
self.nb.custom_fields[device_cf] = None
self.nb.custom_fields[config["device_cf"]] = None
self.nb.save()
def _zabbixHostnameExists(self):
@@ -421,12 +400,13 @@ class PhysicalDevice:
macros = ZabbixUsermacros(
self.nb,
self._usermacro_map(),
usermacro_sync,
config['usermacro_sync'],
logger=self.logger,
host=self.name,
)
if macros.sync is False:
self.usermacros = []
return True
self.usermacros = macros.generate()
return True
@@ -438,10 +418,10 @@ class PhysicalDevice:
tags = ZabbixTags(
self.nb,
self._tag_map(),
tag_sync,
tag_lower,
tag_name=tag_name,
tag_value=tag_value,
config['tag_sync'],
config['tag_lower'],
tag_name=config['tag_name'],
tag_value=config['tag_value'],
logger=self.logger,
host=self.name,
)
@@ -459,7 +439,7 @@ class PhysicalDevice:
input: List of all proxies and proxy groups in standardized format
"""
# check if the key Zabbix is defined in the config context
if not "zabbix" in self.nb.config_context:
if "zabbix" not in self.nb.config_context:
return False
if (
"proxy" in self.nb.config_context["zabbix"]
@@ -557,7 +537,7 @@ class PhysicalDevice:
self.logger.error(msg)
raise SyncExternalError(msg) from e
# Set NetBox custom field to hostID value.
self.nb.custom_fields[device_cf] = int(self.zabbix_id)
self.nb.custom_fields[config["device_cf"]] = int(self.zabbix_id)
self.nb.save()
msg = f"Host {self.name}: Created host in Zabbix."
self.logger.info(msg)
@@ -622,7 +602,7 @@ class PhysicalDevice:
)
self.logger.error(e)
raise SyncExternalError(e) from None
self.logger.info(f"Updated host {self.name} with data {kwargs}.")
self.logger.info(f"Host {self.name}: updated with data {sanatize_log_output(kwargs)}.")
self.create_journal_entry("info", "Updated host in Zabbix with latest NB data.")
def ConsistencyCheck(
@@ -738,10 +718,8 @@ class PhysicalDevice:
# Check if a proxy has been defined
if self.zbxproxy:
# Check if proxy or proxy group is defined
if (
self.zbxproxy["idtype"] in host
and host[self.zbxproxy["idtype"]] == self.zbxproxy["id"]
):
if (self.zbxproxy["idtype"] in host and
host[self.zbxproxy["idtype"]] == self.zbxproxy["id"]):
self.logger.debug(f"Host {self.name}: proxy in-sync.")
# Backwards compatibility for Zabbix <= 6
elif "proxy_hostid" in host and host["proxy_hostid"] == self.zbxproxy["id"]:
@@ -799,7 +777,7 @@ class PhysicalDevice:
else:
self.logger.warning(f"Host {self.name}: inventory_mode OUT of sync.")
self.updateZabbixHost(inventory_mode=str(self.inventory_mode))
if inventory_sync and self.inventory_mode in [0, 1]:
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
# Check host inventory mapping
if host["inventory"] == self.inventory:
self.logger.debug(f"Host {self.name}: inventory in-sync.")
@@ -808,24 +786,33 @@ class PhysicalDevice:
self.updateZabbixHost(inventory=self.inventory)
# Check host usermacros
if usermacro_sync:
macros_filtered = []
# Do not re-sync secret usermacros unless sync is set to 'full'
if str(usermacro_sync).lower() != "full":
for m in deepcopy(self.usermacros):
if m["type"] == str(1):
# Remove the value as the api doesn't return it
# this will allow us to only update usermacros that don't exist
m.pop("value")
macros_filtered.append(m)
if host["macros"] == self.usermacros or host["macros"] == macros_filtered:
if config['usermacro_sync']:
# Make a full copy synce we dont want to lose the original value
# of secret type macros from Netbox
netbox_macros = deepcopy(self.usermacros)
# Set the sync bit
full_sync_bit = bool(str(config['usermacro_sync']).lower() == "full")
for macro in netbox_macros:
# If the Macro is a secret and full sync is NOT activated
if macro["type"] == str(1) and not full_sync_bit:
# Remove the value as the Zabbix api does not return the value key
# This is required when you want to do a diff between both lists
macro.pop("value")
# Sort all lists
def filter_with_macros(macro):
return macro["macro"]
host["macros"].sort(key=filter_with_macros)
netbox_macros.sort(key=filter_with_macros)
# Check if both lists are the same
if host["macros"] == netbox_macros:
self.logger.debug(f"Host {self.name}: usermacros in-sync.")
else:
self.logger.warning(f"Host {self.name}: usermacros OUT of sync.")
# Update Zabbix with NetBox usermacros
self.updateZabbixHost(macros=self.usermacros)
# Check host usermacros
if tag_sync:
# Check host tags
if config['tag_sync']:
if remove_duplicates(host["tags"], sortkey="tag") == self.tags:
self.logger.debug(f"Host {self.name}: tags in-sync.")
else:
@@ -881,7 +868,8 @@ class PhysicalDevice:
try:
# API call to Zabbix
self.zabbix.hostinterface.update(updates)
e = f"Host {self.name}: solved interface conflict."
e = (f"Host {self.name}: updated interface "
f"with data {sanatize_log_output(updates)}.")
self.logger.info(e)
self.create_journal_entry("info", e)
except APIRequestError as e:
@@ -923,7 +911,7 @@ class PhysicalDevice:
self.nb_journals.create(journal)
self.logger.debug(f"Host {self.name}: Created journal entry in NetBox")
return True
except JournalError(e) as e:
except NetboxRequestError as e:
self.logger.warning(
"Unable to create journal entry for "
f"{self.name}: NB returned {e}"