From 053028b283c7d60069754b0215fe08bc0f7d5876 Mon Sep 17 00:00:00 2001 From: TheNetworkGuy Date: Fri, 25 Oct 2024 16:02:08 +0200 Subject: [PATCH] Splitted hostgroup generation logic into its seperate module. Changed hostgroup "dev_role" to "role" for VM role prepration. Started work on basic VM class. --- config.py.example | 4 +- modules/device.py | 60 ++-------------- modules/hostgroups.py | 146 ++++++++++++++++++++++++++++++++++++++ modules/virtualMachine.py | 23 ++++++ netbox_zabbix_sync.py | 2 +- 5 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 modules/hostgroups.py create mode 100644 modules/virtualMachine.py diff --git a/config.py.example b/config.py.example index b856989..0dbe7a9 100644 --- a/config.py.example +++ b/config.py.example @@ -3,7 +3,7 @@ # coming from config context instead of a custom field. templates_config_context = False -# Set to true to give config context templates a +# Set to true to give config context templates a # higher priority then custom field templates templates_config_context_overrule = False @@ -41,7 +41,7 @@ zabbix_device_disable = ["Offline", "Planned", "Staged", "Failed"] # 'Global/Europe/Netherlands/Amsterdam' instead of just 'Amsterdam'. # # traverse_site_groups controls the same behaviour for any assigned site_groups. -hostgroup_format = "site/manufacturer/dev_role" +hostgroup_format = "site/manufacturer/role" traverse_regions = False traverse_site_groups = False diff --git a/modules/device.py b/modules/device.py index df8918d..07319ff 100644 --- a/modules/device.py +++ b/modules/device.py @@ -9,7 +9,7 @@ from zabbix_utils import APIRequestError from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalError, InterfaceConfigError, JournalError) from modules.interface import ZabbixInterface -from modules.tools import build_path +from modules.hostgroups import Hostgroup try: from config import ( template_cf, device_cf, @@ -91,58 +91,12 @@ class NetworkDevice(): def set_hostgroup(self, hg_format, nb_site_groups, nb_regions): """Set the hostgroup for this device""" - # Get all variables from the NB data - dev_location = str(self.nb.location) if self.nb.location else None - # Check the Netbox version. Use backwards compatibility for versions 2 and 3. - if self.nb_api_version.startswith(("2", "3")): - dev_role = self.nb.device_role.name - else: - dev_role = self.nb.role.name - manufacturer = self.nb.device_type.manufacturer.name - region = str(self.nb.site.region) if self.nb.site.region else None - site = self.nb.site.name - site_group = str(self.nb.site.group) if self.nb.site.group else None - tenant = str(self.tenant) if self.tenant else None - tenant_group = str(self.tenant.group) if tenant else None - # Set mapper for string -> variable - hostgroup_vars = {"dev_location": dev_location, "dev_role": dev_role, - "manufacturer": manufacturer, "region": region, - "site": site, "site_group": site_group, - "tenant": tenant, "tenant_group": tenant_group} - # Generate list based off string input format - hg_items = hg_format.split("/") - hostgroup = "" - # Go through all hostgroup items - for item in hg_items: - # Check if the variable (such as Tenant) is empty. - if not hostgroup_vars[item]: - continue - # Check if the item is a custom field name - if item not in hostgroup_vars: - cf_value = self.nb.custom_fields[item] if item in self.nb.custom_fields else None - if cf_value: - # If there is a cf match, add the value of this cf to the hostgroup - hostgroup += cf_value + "/" - # Should there not be a match, this means that - # the variable is invalid. Skip regardless. - continue - # Add value of predefined variable to hostgroup format - if item == "site_group" and nb_site_groups and traverse_site_groups: - group_path = build_path(site_group, nb_site_groups) - hostgroup += "/".join(group_path) + "/" - elif item == "region" and nb_regions and traverse_regions: - region_path = build_path(region, nb_regions) - hostgroup += "/".join(region_path) + "/" - else: - hostgroup += hostgroup_vars[item] + "/" - # If the final hostgroup variable is empty - if not hostgroup: - e = (f"{self.name} has no reliable hostgroup. This is" - "most likely due to the use of custom fields that are empty.") - self.logger.error(e) - raise SyncInventoryError(e) - # Remove final inserted "/" and set hostgroup to class var - self.hostgroup = hostgroup.rstrip("/") + # Create new Hostgroup instance + hg = Hostgroup("dev", self.nb, self.nb_api_version, + traverse_site_groups, traverse_regions, + nb_site_groups, nb_regions) + # Generate hostgroup based on hostgroup format + self.hostgroup = hg.generate(hg_format) def set_template(self, prefer_config_context, overrule_custom): """ Set Template """ diff --git a/modules/hostgroups.py b/modules/hostgroups.py new file mode 100644 index 0000000..5c4f151 --- /dev/null +++ b/modules/hostgroups.py @@ -0,0 +1,146 @@ +"""Module for all hostgroup related code""" +from modules.exceptions import HostgroupError +from modules.tools import build_path + +class Hostgroup(): + """Hostgroup class for devices and VM's + Takes type (vm or dev) and NB object""" + def __init__(self, obj_type, nb_obj, version, + nested_sitegroup_flag, nested_region_flag, + nb_groups, nb_regions): + if obj_type not in ("vm", "dev"): + raise HostgroupError(f"Unable to create hostgroup with type {type}") + self.type = str(obj_type) + self.nb = nb_obj + self.name = self.nb.name + self.nb_version = version + # Used for nested data objects + self.nested_objects = {"site_group": {"flag": nested_sitegroup_flag, "data": nb_groups}, + "region": {"flag": nested_region_flag, "data": nb_regions}} + self._set_format_options() + + def __str__(self): + return f"Hostgroup for {self.type} {self.name}" + + def __repr__(self): + return self.__str__() + + def _set_format_options(self): + """ + Set all available variables + for hostgroup generation + """ + format_options = {} + # Set variables for both type of devices + if self.type in ("vm", "dev"): + # Role fix for Netbox <=3 + role = None + if self.nb_version.startswith(("2", "3")) and self.type == "dev": + role = self.nb.device_role.name + else: + role = self.nb.role.name + # Add default formatting options + # Check if a site is configured. A site is optional for VMs + format_options["region"] = None + format_options["location"] = None + format_options["site_group"] = None + if self.nb.site: + if self.nb.site.region: + format_options["region"] = self.generate_parents("region", + str(self.nb.site.region)) + if self.nb.location: + format_options["location"] = str(self.nb.location) + if self.nb.site.group: + format_options["site_group"] = self.generate_parents("site_group", + str(self.nb.site.group)) + format_options["role"] = role + format_options["site"] = self.nb.site.name if self.nb.site else None + format_options["tenant"] = str(self.nb.tenant) if self.nb.tenant else None + format_options["tenant_group"] = str(self.tenant.group) if self.nb.tenant else None + format_options["platform"] = self.nb.platform.name if self.nb.platform else None + # Variables only applicable for devices + if self.type == "dev": + format_options["manufacturer"] = self.nb.device_type.manufacturer.name + # Variables only applicable for VM's + if self.type == "vm": + format_options["cluster"] = str(self.nb.cluster.name) if self.nb.cluster else None + self.format_options = format_options + + def generate(self, hg_format=None): + """Generate hostgroup based on a provided format""" + # Set format to default in case its not specified + if not hg_format: + hg_format = "site/manufacturer/role" if self.type == "dev" else "cluster/role" + # Split all given names + hg_output = list() + hg_items = hg_format.split("/") + for hg_item in hg_items: + # Check if requested data is available as option for this host + if hg_item not in self.format_options: + # Check if a custom field exists with this name + cf_data = self.custom_field_lookup(hg_item) + # CF does not exist + if not cf_data["result"]: + raise HostgroupError(f"Unable to generate hostgroup for host {self.name}. " + f"Item type {hg_item} not supported.") + # CF data is populated + if cf_data["cf"]: + hg_output.append(cf_data["cf"]) + continue + # Check if there is a value associated to the variable. + # For instance, if a device has no location, do not use it with hostgroup calculation + hostgroup_value = self.format_options[hg_item] + if hostgroup_value: + hg_output.append(hostgroup_value) + # Check if the hostgroup is populated with at least one item. + if bool(hg_output): + return "/".join(hg_output) + raise HostgroupError(f"Unable to generate hostgroup for host {self.name}." + "Not enough valid items. This is most likely" + "due to the use of custom fields that are empty.") + + def list_formatoptions(self): + """ + Function to easily troubleshoot which values + are generated for a specific device or VM. + """ + print(f"The following options are available for host {self.name}") + for option_type, value in self.format_options.items(): + if value is not None: + print(f"{option_type} - {value}") + print("The following options are not available") + for option_type, value in self.format_options.items(): + if value is None: + print(f"{option_type}") + + def custom_field_lookup(self, hg_category): + """ + Checks if a valid custom field is present in Netbox. + INPUT: Custom field name + OUTPUT: dictionary with 'result' and 'cf' keys. + """ + # Check if the custom field exists + if hg_category not in self.nb.custom_fields: + return {"result": False, "cf": None} + # Checks if the custom field has been populated + if not bool(self.nb.custom_fields[hg_category]): + return {"result": True, "cf": None} + # Custom field exists and is populated + return {"result": True, "cf": self.nb.custom_fields[hg_category]} + + def generate_parents(self, nest_type, child_object): + """ + Generates parent objects to implement nested regions / nested site groups + INPUT: nest_type to set which type of nesting is going to be processed + child_object: the name of the child object (for instance the last NB region) + OUTPUT: STRING - Either the single child name or child and parents. + """ + # Check if this type of nesting is supported. + if not nest_type in self.nested_objects: + return child_object + # If the nested flag is True, perform parent calculation + if self.nested_objects[nest_type]["flag"]: + final_nested_object = build_path(child_object, self.nested_objects[nest_type]["data"]) + return "/".join(final_nested_object) + # Nesting is not allowed for this object. Return child_object + return child_object diff --git a/modules/virtualMachine.py b/modules/virtualMachine.py new file mode 100644 index 0000000..82ca3d3 --- /dev/null +++ b/modules/virtualMachine.py @@ -0,0 +1,23 @@ +"""Module that hosts all functions for virtual machine processing""" + +from modules.exceptions import * + +class VirtualMachine(): + """Model for virtual machines""" + def __init__(self, nb, name): + self.nb = nb + self.name = name + self.hostgroup = None + + def __repr__(self): + return self.name + + def __str__(self): + return self.__repr__() + + def _data_prep(self): + self.platform = self.nb.platform.name + self.cluster = self.nb.cluster.name + + def set_hostgroup(self): + self.hostgroup = "Virtual machines" diff --git a/netbox_zabbix_sync.py b/netbox_zabbix_sync.py index ea95bce..52f9652 100755 --- a/netbox_zabbix_sync.py +++ b/netbox_zabbix_sync.py @@ -76,7 +76,7 @@ def main(arguments): netbox = api(netbox_host, token=netbox_token, threading=True) # Check if the provided Hostgroup layout is valid hg_objects = hostgroup_format.split("/") - allowed_objects = ["dev_location", "dev_role", "manufacturer", "region", + allowed_objects = ["dev_location", "role", "manufacturer", "region", "site", "site_group", "tenant", "tenant_group"] # Create API call to get all custom fields which are on the device objects device_cfs = netbox.extras.custom_fields.filter(type="text", content_type_id=23)