diff --git a/config.py.example b/config.py.example index 68f6fea..e4082e6 100644 --- a/config.py.example +++ b/config.py.example @@ -121,3 +121,33 @@ vm_usermacro_map = {"memory": "{$TOTAL_MEMORY}", "url": "{$NB_URL}", "id": "{$NB_ID}"} +# To sync host tags to Zabbix, set to True. +tag_sync = False + +# Setting tag_lower to True will lower capital letters ain tag names and values +# This is more inline with the Zabbix way of working with tags. +# +# You can however set this to False to ensure capital letters are synced to Zabbix tags. +tag_lower = True + +# We can sync NetBox device/VM tags to Zabbix, but as NetBox tags don't follow the key/value +# pattern, we need to specify a tag name to register the NetBox tags in Zabbix. +# +# +# +# If tag_name is set to False, we won't sync NetBox device/VM tags to Zabbix. +tag_name = 'NetBox' + +# We can choose to use 'name', 'slug' or 'display' NetBox tag properties as a value in Zabbix. +# 'name'is used by default. +tag_value = "name" + +# device tag_map to map NetBox fields to host tags. +device_tag_map = {"site/name": "site", + "rack/name": "rack", + "platform/name": "target"} + +# Virtual machine tag_map to map NetBox fields to host tags. +vm_tag_map = {"site/name": "site", + "cluster/name": "cluster", + "platform/name": "target"} diff --git a/modules/device.py b/modules/device.py index 76f07cf..4ec96b5 100644 --- a/modules/device.py +++ b/modules/device.py @@ -12,8 +12,9 @@ from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalE InterfaceConfigError, JournalError) from modules.interface import ZabbixInterface from modules.usermacros import ZabbixUsermacros +from modules.tags import ZabbixTags from modules.hostgroups import Hostgroup -from modules.tools import field_mapper +from modules.tools import field_mapper, remove_duplicates try: from config import ( @@ -24,7 +25,12 @@ try: inventory_mode, device_inventory_map, usermacro_sync, - device_usermacro_map + device_usermacro_map, + tag_sync, + tag_lower, + tag_name, + tag_value, + device_tag_map ) except ModuleNotFoundError: print("Configuration file config.py not found in main directory." @@ -60,6 +66,7 @@ class PhysicalDevice(): self.inventory_mode = -1 self.inventory = {} self.usermacros = {} + self.tags = {} self.logger = logger if logger else getLogger(__name__) self._setBasics() @@ -77,6 +84,10 @@ class PhysicalDevice(): """ Use device inventory maps """ return device_usermacro_map + def _tag_map(self): + """ Use device host tag maps """ + return device_tag_map + def _setBasics(self): """ Sets basic information like IP address. @@ -362,6 +373,21 @@ class PhysicalDevice(): self.usermacros = macros.generate() return True + + def set_tags(self): + """ + Generates Host Tags + """ + tags = ZabbixTags(self.nb, self._tag_map(), + tag_sync, tag_lower, tag_name=tag_name, + tag_value=tag_value, logger=self.logger, + host=self.name) + if tags.sync is False: + self.tags = [] + + self.tags = tags.generate() + return True + def setProxy(self, proxy_list): """ Sets proxy or proxy group if this @@ -432,7 +458,8 @@ class PhysicalDevice(): "description": description, "inventory_mode": self.inventory_mode, "inventory": self.inventory, - "macros": self.usermacros + "macros": self.usermacros, + "tags": self.tags } # If a Zabbix proxy or Zabbix Proxy group has been defined if self.zbxproxy: @@ -548,7 +575,8 @@ class PhysicalDevice(): selectHostGroups=["groupid"], selectParentTemplates=["templateid"], selectInventory=list(self._inventory_map().values()), - selectMacros=["macro","value","type","description"] + selectMacros=["macro","value","type","description"], + selectTags=["tag","value"] ) if len(host) > 1: e = (f"Got {len(host)} results for Zabbix hosts " @@ -598,9 +626,8 @@ class PhysicalDevice(): if group["groupid"] == self.group_id: self.logger.debug(f"Host {self.name}: hostgroup in-sync.") break - else: - self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.") - self.updateZabbixHost(groups={'groupid': self.group_id}) + self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.") + self.updateZabbixHost(groups={'groupid': self.group_id}) if int(host["status"]) == self.zabbix_state: self.logger.debug(f"Host {self.name}: status in-sync.") @@ -688,6 +715,14 @@ class PhysicalDevice(): self.logger.warning(f"Host {self.name}: usermacros OUT of sync.") self.updateZabbixHost(macros=self.usermacros) + # Check host usermacros + if tag_sync: + if remove_duplicates(host['tags'],sortkey='tag') == self.tags: + self.logger.debug(f"Host {self.name}: tags in-sync.") + else: + self.logger.warning(f"Host {self.name}: tags OUT of sync.") + self.updateZabbixHost(tags=self.tags) + # If only 1 interface has been found # pylint: disable=too-many-nested-blocks if len(host['interfaces']) == 1: diff --git a/modules/tags.py b/modules/tags.py new file mode 100644 index 0000000..4993cd3 --- /dev/null +++ b/modules/tags.py @@ -0,0 +1,117 @@ +#!/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 +""" +from logging import getLogger +from modules.tools import field_mapper, remove_duplicates + +class ZabbixTags(): + """Class that represents a Zabbix interface.""" + + def __init__(self, nb, tag_map, tag_sync, tag_lower=True, + tag_name=None, tag_value=None, logger=None, host=None): + self.nb = nb + self.name = host if host else nb.name + self.tag_map = tag_map + self.logger = logger if logger else getLogger(__name__) + self.tags = {} + self.lower = tag_lower + self.tag_name = tag_name + self.tag_value = tag_value + self.tag_sync = tag_sync + self.sync = False + self._set_config() + + def __repr__(self): + return self.name + + def __str__(self): + return self.__repr__() + + def _set_config(self): + """ + Setup class + """ + if self.tag_sync: + self.sync = True + + return True + + def validate_tag(self, tag_name): + """ + Validates tag name + """ + if tag_name and isinstance(tag_name, str) and len(tag_name)<=256: + return True + return False + + def validate_value(self, tag_value): + """ + Validates tag value + """ + if tag_value and isinstance(tag_value, str) and len(tag_value)<=256: + return True + return False + + def render_tag(self, tag_name, tag_value): + """ + Renders a tag + """ + tag={} + if self.validate_tag(tag_name): + if self.lower: + tag['tag'] = tag_name.lower() + else: + tag['tag'] = tag_name + else: + self.logger.error(f'Tag {tag_name} is not a valid tag name, skipping.') + return False + + if self.validate_value(tag_value): + if self.lower: + tag['value'] = tag_value.lower() + else: + tag['value'] = tag_value + else: + self.logger.error(f'Tag {tag_name} has an invalid value: \'{tag_value}\', skipping.') + return False + return tag + + def generate(self): + """ + Generate full set of Usermacros + """ + # pylint: disable=too-many-branches + tags=[] + # Parse the field mapper for tags + if self.tag_map: + self.logger.debug(f"Host {self.nb.name}: Starting tag mapper") + field_tags = field_mapper(self.nb.name, self.tag_map, self.nb, self.logger) + for tag, value in field_tags.items(): + t = self.render_tag(tag, value) + if t: + tags.append(t) + + # Parse NetBox config context for tags + if ("zabbix" in self.nb.config_context and "tags" in self.nb.config_context['zabbix'] + and isinstance(self.nb.config_context['zabbix']['tags'], list)): + for tag in self.nb.config_context['zabbix']['tags']: + if isinstance(tag, dict): + for tagname, value in tag.items(): + t = self.render_tag(tagname, value) + if t: + tags.append(t) + + # Pull in NetBox device tags if tag_name is set + if self.tag_name and isinstance(self.tag_name, str): + for tag in self.nb.tags: + if self.tag_value.lower() in ['display', 'name', 'slug']: + value = tag[self.tag_value] + else: + value = tag['name'] + t = self.render_tag(self.tag_name, value) + if t: + tags.append(t) + + return remove_duplicates(tags, sortkey='tag') diff --git a/modules/tools.py b/modules/tools.py index f32e802..8d658a3 100644 --- a/modules/tools.py +++ b/modules/tools.py @@ -76,3 +76,14 @@ def field_mapper(host, mapper, nbdevice, logger): logger.debug(f"Host {host}: Field mapping complete. " f"Mapped {len(list(filter(None, data.values())))} field(s)") return data + +def remove_duplicates(input_list, sortkey=None): + """ + Removes duplicate entries from a list and sorts the list + """ + output_list = [] + if isinstance(input_list, list): + output_list = [dict(t) for t in {tuple(d.items()) for d in input_list}] + if sortkey and isinstance(sortkey, str): + output_list.sort(key=lambda x: x[sortkey]) + return output_list diff --git a/modules/virtual_machine.py b/modules/virtual_machine.py index 5afdb18..273f9e7 100644 --- a/modules/virtual_machine.py +++ b/modules/virtual_machine.py @@ -11,6 +11,7 @@ try: from config import ( vm_inventory_map, vm_usermacro_map, + vm_tag_map, traverse_site_groups, traverse_regions ) @@ -31,9 +32,13 @@ class VirtualMachine(PhysicalDevice): return vm_inventory_map def _usermacro_map(self): - """ use VM inventory maps """ + """ use VM usermacro maps """ return vm_usermacro_map + def _tag_map(self): + """ use VM tag maps """ + return vm_tag_map + def set_hostgroup(self, hg_format, nb_site_groups, nb_regions): """Set the hostgroup for this device""" # Create new Hostgroup instance diff --git a/netbox_zabbix_sync.py b/netbox_zabbix_sync.py index 5498edc..04a4e07 100755 --- a/netbox_zabbix_sync.py +++ b/netbox_zabbix_sync.py @@ -173,6 +173,7 @@ def main(arguments): continue vm.set_inventory(nb_vm) vm.set_usermacros() + vm.set_tags() # Checks if device is in cleanup state if vm.status in zabbix_device_removal: if vm.zabbix_id: @@ -227,6 +228,7 @@ def main(arguments): continue device.set_inventory(nb_device) device.set_usermacros() + device.set_tags() # Checks if device is part of cluster. # Requires clustering variable if device.isCluster() and clustering: