Full usermacro support

This commit is contained in:
Raymond Kuiper 2025-02-14 15:18:26 +01:00
parent 1b831a2d39
commit eea7df660a
5 changed files with 73 additions and 88 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines # pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines, too-many-public-methods
""" """
Device specific handeling for NetBox to Zabbix Device specific handeling for NetBox to Zabbix
""" """
@ -9,12 +9,11 @@ from copy import deepcopy
from logging import getLogger from logging import getLogger
from zabbix_utils import APIRequestError from zabbix_utils import APIRequestError
from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalError, from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalError,
InterfaceConfigError, JournalError, UsermacroError) InterfaceConfigError, JournalError)
from modules.interface import ZabbixInterface from modules.interface import ZabbixInterface
from modules.usermacros import ZabbixUsermacros from modules.usermacros import ZabbixUsermacros
from modules.hostgroups import Hostgroup from modules.hostgroups import Hostgroup
from modules.tools import field_mapper from modules.tools import field_mapper
from pprint import pprint
try: try:
from config import ( from config import (
@ -73,7 +72,7 @@ class PhysicalDevice():
def _inventory_map(self): def _inventory_map(self):
""" Use device inventory maps """ """ Use device inventory maps """
return device_inventory_map return device_inventory_map
def _usermacro_map(self): def _usermacro_map(self):
""" Use device inventory maps """ """ Use device inventory maps """
return device_usermacro_map return device_usermacro_map
@ -197,31 +196,6 @@ class PhysicalDevice():
if inventory_sync and self.inventory_mode in [0,1]: if inventory_sync and self.inventory_mode in [0,1]:
self.logger.debug(f"Host {self.name}: Starting inventory mapper") self.logger.debug(f"Host {self.name}: Starting inventory mapper")
self.inventory = field_mapper(self.name, self._inventory_map(), nbdevice, self.logger) self.inventory = field_mapper(self.name, self._inventory_map(), nbdevice, self.logger)
# # Let's build an inventory dict for each property in the inventory_map
# for nb_inv_field, zbx_inv_field in self._inventory_map().items():
# field_list = nb_inv_field.split("/") # convert str to list based on delimiter
# # start at the base of the dict...
# value = nbdevice
# # ... and step through the dict till we find the needed value
# for item in field_list:
# value = value[item] if value else None
# # Check if the result is usable and expected
# # We want to apply any int or float 0 values,
# # even if python thinks those are empty.
# if ((value and isinstance(value, int | float | str )) or
# (isinstance(value, int | float) and int(value) ==0)):
# self.inventory[zbx_inv_field] = str(value)
# elif not value:
# # empty value should just be an empty string for API compatibility
# self.logger.debug(f"Host {self.name}: NetBox inventory lookup for "
# f"'{nb_inv_field}' returned an empty value")
# self.inventory[zbx_inv_field] = ""
# else:
# # Value is not a string or numeral, probably not what the user expected.
# self.logger.error(f"Host {self.name}: Inventory lookup for '{nb_inv_field}'"
# " returned an unexpected type: it will be skipped.")
# self.logger.debug(f"Host {self.name}: Inventory mapping complete. "
# f"Mapped {len(list(filter(None, self.inventory.values())))} field(s)")
return True return True
def isCluster(self): def isCluster(self):
@ -375,14 +349,19 @@ class PhysicalDevice():
self.logger.warning(message) self.logger.warning(message)
raise SyncInventoryError(message) from e raise SyncInventoryError(message) from e
def setUsermacros(self): def set_usermacros(self):
# Initiate Usermacros class """
macros = ZabbixUsermacros(self.nb.config_context, self._usermacro_map()) Generates Usermacros
if macros.sync == False: """
return [] macros = ZabbixUsermacros(self.nb, self._usermacro_map(),
else: usermacro_sync, logger=self.logger,
return macros.generate() host=self.name)
if macros.sync is False:
self.usermacros = []
self.usermacros = macros.generate()
return True
def setProxy(self, proxy_list): def setProxy(self, proxy_list):
""" """
Sets proxy or proxy group if this Sets proxy or proxy group if this
@ -443,8 +422,6 @@ class PhysicalDevice():
groups = [{"groupid": self.group_id}] groups = [{"groupid": self.group_id}]
# Set Zabbix proxy if defined # Set Zabbix proxy if defined
self.setProxy(proxies) self.setProxy(proxies)
# Set usermacros
self.usermacros = self.setUsermacros()
# Set basic data for host creation # Set basic data for host creation
create_data = {"host": self.name, create_data = {"host": self.name,
"name": self.visible_name, "name": self.visible_name,
@ -571,7 +548,7 @@ class PhysicalDevice():
selectHostGroups=["groupid"], selectHostGroups=["groupid"],
selectParentTemplates=["templateid"], selectParentTemplates=["templateid"],
selectInventory=list(self._inventory_map().values()), selectInventory=list(self._inventory_map().values()),
selectMacros=["macro","value","type","description"] selectMacros=["macro","value","type","description"]
) )
if len(host) > 1: if len(host) > 1:
e = (f"Got {len(host)} results for Zabbix hosts " e = (f"Got {len(host)} results for Zabbix hosts "
@ -621,9 +598,9 @@ class PhysicalDevice():
if group["groupid"] == self.group_id: if group["groupid"] == self.group_id:
self.logger.debug(f"Host {self.name}: hostgroup in-sync.") self.logger.debug(f"Host {self.name}: hostgroup in-sync.")
break break
else: else:
self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.") self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.")
self.updateZabbixHost(groups={'groupid': self.group_id}) self.updateZabbixHost(groups={'groupid': self.group_id})
if int(host["status"]) == self.zabbix_state: if int(host["status"]) == self.zabbix_state:
self.logger.debug(f"Host {self.name}: status in-sync.") self.logger.debug(f"Host {self.name}: status in-sync.")
@ -697,14 +674,13 @@ class PhysicalDevice():
# Check host usermacros # Check host usermacros
if usermacro_sync: if usermacro_sync:
macros_filtered = [] macros_filtered = []
self.usermacros = self.setUsermacros()
# Do not re-sync secret usermacros unless sync is set to 'full' # Do not re-sync secret usermacros unless sync is set to 'full'
if not str(usermacro_sync).lower() == "full": if str(usermacro_sync).lower() != "full":
for m in deepcopy(self.usermacros): for m in deepcopy(self.usermacros):
if m['type'] == str(1): if m['type'] == str(1):
# Remove the value as the api doesn't return it # Remove the value as the api doesn't return it
# this will allow us to only update usermacros that don't exist # this will allow us to only update usermacros that don't exist
m.pop('value') m.pop('value')
macros_filtered.append(m) macros_filtered.append(m)
if host['macros'] == self.usermacros or host['macros'] == macros_filtered: if host['macros'] == self.usermacros or host['macros'] == macros_filtered:
self.logger.debug(f"Host {self.name}: usermacros in-sync.") self.logger.debug(f"Host {self.name}: usermacros in-sync.")

View File

@ -1,6 +1,5 @@
from logging import getLogger
"""A collection of tools used by several classes""" """A collection of tools used by several classes"""
def convert_recordset(recordset): def convert_recordset(recordset):
""" Converts netbox RedcordSet to list of dicts. """ """ Converts netbox RedcordSet to list of dicts. """
recordlist = [] recordlist = []
@ -45,7 +44,6 @@ def proxy_prepper(proxy_list, proxy_group_list):
output.append(group) output.append(group)
return output return output
def field_mapper(host, mapper, nbdevice, logger): def field_mapper(host, mapper, nbdevice, logger):
""" """
Maps NetBox field data to Zabbix properties. Maps NetBox field data to Zabbix properties.
@ -75,6 +73,6 @@ def field_mapper(host, mapper, nbdevice, logger):
# Value is not a string or numeral, probably not what the user expected. # Value is not a string or numeral, probably not what the user expected.
logger.error(f"Host {host}: Lookup for '{nb_field}'" logger.error(f"Host {host}: Lookup for '{nb_field}'"
" returned an unexpected type: it will be skipped.") " returned an unexpected type: it will be skipped.")
logger.debug(f"Host {host}: Field mapping complete." logger.debug(f"Host {host}: Field mapping complete. "
f"Mapped {len(list(filter(None, data.values())))} field(s)") f"Mapped {len(list(filter(None, data.values())))} field(s)")
return data return data

View File

@ -1,71 +1,71 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments, logging-fstring-interpolation
""" """
All of the Zabbix Usermacro related configuration All of the Zabbix Usermacro related configuration
""" """
from re import match from re import match
from logging import getLogger from logging import getLogger
from zabbix_utils import APIRequestError from modules.tools import field_mapper
from modules.exceptions import UsermacroError
from pprint import pprint
try:
from config import (
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)
class ZabbixUsermacros(): class ZabbixUsermacros():
"""Class that represents a Zabbix interface.""" """Class that represents a Zabbix interface."""
def __init__(self, context, usermacro_map, logger=None): def __init__(self, nb, usermacro_map, usermacro_sync, logger=None, host=None):
self.context = context self.nb = nb
self.name = host if host else nb.name
self.usermacro_map = usermacro_map self.usermacro_map = usermacro_map
self.logger = logger if logger else getLogger(__name__) self.logger = logger if logger else getLogger(__name__)
self.usermacros = {} self.usermacros = {}
self.usermacro_sync = usermacro_sync
self.sync = False self.sync = False
self.force_sync = False self.force_sync = False
self._setConfig() self._set_config()
def __repr__(self): def __repr__(self):
return self.name return self.name
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
def _setConfig(self): def _set_config(self):
if str(usermacro_sync).lower() == "full": """
Setup class
"""
if str(self.usermacro_sync).lower() == "full":
self.sync = True self.sync = True
self.force_sync = True self.force_sync = True
elif usermacro_sync: elif self.usermacro_sync:
self.sync = True self.sync = True
return True return True
def validate_macro(self, macro_name): def validate_macro(self, macro_name):
pattern = '\{\$[A-Z0-9\._]*(\:.*)?\}' """
Validates usermacro name
"""
pattern = r'\{\$[A-Z0-9\._]*(\:.*)?\}'
return match(pattern, macro_name) return match(pattern, macro_name)
def render_macro(self, macro_name, macro_properties): def render_macro(self, macro_name, macro_properties):
"""
Renders a full usermacro from partial input
"""
macro={} macro={}
macrotypes={'text': 0, 'secret': 1, 'vault': 2} macrotypes={'text': 0, 'secret': 1, 'vault': 2}
if self.validate_macro(macro_name): if self.validate_macro(macro_name):
macro['macro'] = str(macro_name) macro['macro'] = str(macro_name)
if isinstance(macro_properties, dict): if isinstance(macro_properties, dict):
if not 'value' in macro_properties: if not 'value' in macro_properties:
self.logger.error(f'Usermacro {macro_name} has no value, skipping.') self.logger.error(f'Usermacro {macro_name} has no value, skipping.')
return False return False
else: macro['value'] = macro_properties['value']
macro['value'] = macro_properties['value']
if 'type' in macro_properties and macro_properties['type'].lower() in macrotypes: if 'type' in macro_properties and macro_properties['type'].lower() in macrotypes:
macro['type'] = str(macrotypes[macro_properties['type']]) macro['type'] = str(macrotypes[macro_properties['type']])
else: else:
macro['type'] = str(0) macro['type'] = str(0)
if 'description' in macro_properties and isinstance(macro_properties['description'], str): if ('description' in macro_properties and
isinstance(macro_properties['description'], str)):
macro['description'] = macro_properties['description'] macro['description'] = macro_properties['description']
else: else:
macro['description'] = "" macro['description'] = ""
@ -78,13 +78,24 @@ class ZabbixUsermacros():
self.logger.error(f'Usermacro {macro_name} is not a valid usermacro name, skipping.') self.logger.error(f'Usermacro {macro_name} is not a valid usermacro name, skipping.')
return False return False
return macro return macro
def generate(self): def generate(self):
"""
Generate full set of Usermacros
"""
macros=[] macros=[]
if "zabbix" in self.context and "usermacros" in self.context['zabbix']: # Parse the field mapper for usermacros
for macro, properties in self.context['zabbix']['usermacros'].items(): if self.usermacro_map:
m = self.render_macro(macro, properties) self.logger.debug(f"Host {self.nb.name}: Starting usermacro mapper")
pprint(m) field_macros = field_mapper(self.nb.name, self.usermacro_map, self.nb, self.logger)
for macro, value in field_macros.items():
m = self.render_macro(macro, value)
if m: if m:
macros.append(m) macros.append(m)
# Parse NetBox config context for usermacros
if "zabbix" in self.nb.config_context and "usermacros" in self.nb.config_context['zabbix']:
for macro, properties in self.nb.config_context['zabbix']['usermacros'].items():
m = self.render_macro(macro, properties)
if m:
macros.append(m)
return macros return macros

View File

@ -9,8 +9,6 @@ from modules.interface import ZabbixInterface
from modules.exceptions import TemplateError, InterfaceConfigError, SyncInventoryError from modules.exceptions import TemplateError, InterfaceConfigError, SyncInventoryError
try: try:
from config import ( from config import (
inventory_sync,
inventory_mode,
vm_inventory_map, vm_inventory_map,
traverse_site_groups, traverse_site_groups,
traverse_regions traverse_regions

View File

@ -161,7 +161,7 @@ def main(arguments):
try: try:
vm = VirtualMachine(nb_vm, zabbix, netbox_journals, nb_version, vm = VirtualMachine(nb_vm, zabbix, netbox_journals, nb_version,
create_journal, logger) create_journal, logger)
logger.debug(f"Host {vm.name}: started operations on VM.") logger.debug(f"Host {vm.name}: Started operations on VM.")
vm.set_vm_template() vm.set_vm_template()
# Check if a valid template has been found for this VM. # Check if a valid template has been found for this VM.
if not vm.zbx_template_names: if not vm.zbx_template_names:
@ -172,6 +172,7 @@ def main(arguments):
if not vm.hostgroup: if not vm.hostgroup:
continue continue
vm.set_inventory(nb_vm) vm.set_inventory(nb_vm)
vm.set_usermacros()
# Checks if device is in cleanup state # Checks if device is in cleanup state
if vm.status in zabbix_device_removal: if vm.status in zabbix_device_removal:
if vm.zabbix_id: if vm.zabbix_id:
@ -225,6 +226,7 @@ def main(arguments):
if not device.hostgroup: if not device.hostgroup:
continue continue
device.set_inventory(nb_device) device.set_inventory(nb_device)
device.set_usermacros()
# Checks if device is part of cluster. # Checks if device is part of cluster.
# Requires clustering variable # Requires clustering variable
if device.isCluster() and clustering: if device.isCluster() and clustering: