From 92711a92ed0085d930ed464bb8c5c8faeae1972f Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Thu, 20 Mar 2025 22:41:53 +0100 Subject: [PATCH 01/20] enhances NetBox version detection #446 --- module/netbox/connection.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/module/netbox/connection.py b/module/netbox/connection.py index 5c86d26..d3a0dd5 100644 --- a/module/netbox/connection.py +++ b/module/netbox/connection.py @@ -196,15 +196,29 @@ def get_api_version(self): str: NetBox API version """ response = None + result = None + try: response = self.session.get( - self.url, + f"{self.url}/status", timeout=self.settings.timeout, verify=self.settings.validate_tls_certs) except Exception as e: do_error_exit(f"NetBox connection: {e}") - result = str(response.headers.get("API-Version")) + # noinspection PyBroadException + try: + result = grab(response.json(), "netbox-version").split("-")[0] + except Exception: + pass + + if not isinstance(result, str): + result = str(response.headers.get("API-Version")) + + try: + version.parse(result) + except Exception as e: + do_error_exit(f"Unable to parse NetBox version '{result}': {e}") log.info(f"Successfully connected to NetBox '{self.settings.host_fqdn}'") log.debug(f"Detected NetBox API version: {result}") From 720a3ba47a78ca1144e379d13d66a9642808ef86 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 21 Mar 2025 07:39:55 +0100 Subject: [PATCH 02/20] fixes issue with retrieving platform name for newer guest tools #448 --- module/sources/vmware/connection.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 9ab8974..1b3be80 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -2180,10 +2180,13 @@ def add_virtual_machine(self, obj): platform = get_string_or_none(grab(obj, "guest.guestFullName", fallback=platform)) # extract prettyName from extraConfig exposed by guest tools - extra_config = [x.value for x in grab(obj, "config.extraConfig", fallback=[]) - if x.key == "guestOS.detailed.data"] - if len(extra_config) > 0: - pretty_name = [x for x in quoted_split(extra_config[0].replace("' ", "', ")) if x.startswith("prettyName")] + extra_config = {x.key: x.value for x in grab(obj, "config.extraConfig", fallback=[]) + if x.key in ["guestOS.detailed.data", "guestInfo.detailed.data"]} + + # first try 'guestInfo.detailed.data' and then 'guestOS.detailed.data' + detailed_data = extra_config.get("guestInfo.detailed.data") or extra_config.get("guestOS.detailed.data") + if isinstance(detailed_data, str): + pretty_name = [x for x in quoted_split(detailed_data.replace("' ", "', ")) if x.startswith("prettyName")] if len(pretty_name) > 0: platform = pretty_name[0].replace("prettyName='","") From edf0495e64fc143fe6d43edb4ecd6b1266c97da1 Mon Sep 17 00:00:00 2001 From: Littlericket <1573629+Littlericket@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:33:51 +0100 Subject: [PATCH 03/20] fix: handle object and multi-object custom fields correctly --- module/netbox/object_classes.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index 96df5fd..9d25093 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -683,6 +683,29 @@ def update(self, data=None, read_from_netbox=False, source=None): if self.data_model.get(key) == NBCustomField: if current_value is None: current_value = dict() + + # Fix for object/multi-object custom fields + # When patching, we only need the IDs, not the full object representation + new_value_copy = new_value.copy() + for field_name, field_value in new_value_copy.items(): + # Check for custom field type + custom_field = self.inventory.get_by_data(NBCustomField, data={"name": field_name}) + if custom_field is not None: + field_type = grab(custom_field, "data.type") + + # Handle object type custom fields - need only ID + if field_type == "object" and isinstance(field_value, dict) and field_value.get('id') is not None: + new_value[field_name] = field_value.get('id') + + # Handle multi-object type custom fields - need list of IDs + elif field_type == "multi-object" and isinstance(field_value, list): + ids = [] + for item in field_value: + if isinstance(item, dict) and item.get('id') is not None: + ids.append(item.get('id')) + if ids: + new_value[field_name] = ids + new_value = {**current_value, **new_value} new_value_str = str(new_value) elif isinstance(new_value, (NetBoxObject, NBObjectList)): @@ -1288,7 +1311,7 @@ def __init__(self, *args, **kwargs): "object_types": list, # field name (object_types) for NetBox < 4.0.0 "content_types": list, - "type": ["text", "longtext", "integer", "boolean", "date", "url", "json", "select", "multiselect"], + "type": ["text", "longtext", "integer", "boolean", "date", "url", "json", "select", "multiselect", "object", "multi-object"], "name": 50, "label": 50, "description": 200, From 7c14187ef2a41c1cde9b00b9f08b392af2d0925f Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 9 May 2025 13:05:06 +0200 Subject: [PATCH 04/20] fixes issue with type hints for python <3.10.0 setups #456 --- module/sources/common/source_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module/sources/common/source_base.py b/module/sources/common/source_base.py index fc81a16..0e95e5c 100644 --- a/module/sources/common/source_base.py +++ b/module/sources/common/source_base.py @@ -8,6 +8,7 @@ # repository or visit: . import re +from typing import Union,Optional from ipaddress import ip_interface, ip_address, IPv6Address, IPv4Address, IPv6Network, IPv4Network from packaging import version @@ -179,7 +180,7 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac return return_data - def return_longest_matching_prefix_for_ip(self, ip_to_match=None, site_name=None) -> NBPrefix|None: + def return_longest_matching_prefix_for_ip(self, ip_to_match=None, site_name=None) -> Optional[NBPrefix]: """ This is a lazy approach to find the longest matching prefix to an IP address. If site_name is set only IP prefixes from that site are matched. @@ -716,7 +717,7 @@ def patch_data(object_to_patch, data, overwrite=False): return data_to_update - def add_vlan_group(self, vlan_data, vlan_site, vlan_cluster) -> NBVLAN | dict: + def add_vlan_group(self, vlan_data, vlan_site, vlan_cluster) -> Union[NBVLAN ,dict]: """ This function will try to find a matching VLAN group according to the settings. Name matching will take precedence over ID matching. First match wins. From cee3425e0c628f9b6894ba65b649ea81b998de0f Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 17 Jun 2025 15:06:26 +0200 Subject: [PATCH 05/20] adds distroVersion to VM linux platform #448 --- module/sources/vmware/connection.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 1b3be80..497f04f 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -2186,9 +2186,15 @@ def add_virtual_machine(self, obj): # first try 'guestInfo.detailed.data' and then 'guestOS.detailed.data' detailed_data = extra_config.get("guestInfo.detailed.data") or extra_config.get("guestOS.detailed.data") if isinstance(detailed_data, str): - pretty_name = [x for x in quoted_split(detailed_data.replace("' ", "', ")) if x.startswith("prettyName")] - if len(pretty_name) > 0: - platform = pretty_name[0].replace("prettyName='","") + detailed_data_dict = dict() + for detailed_data_item in quoted_split(detailed_data.replace("' ", "', ")): + detailed_data_key, detailed_data_value = detailed_data_item.split("=") + detailed_data_dict[detailed_data_key] = detailed_data_value.strip("'") + if len(detailed_data_dict.get("prettyName","")) > 0: + platform = detailed_data_dict.get("prettyName") + if detailed_data_dict.get("prettyName").lower() == "linux" and \ + detailed_data_dict.get("distroVersion") not in platform: + platform = f'{platform} {detailed_data_dict.get("distroVersion")}' if platform is not None: platform = self.get_object_relation(platform, "vm_platform_relation", fallback=platform) From 15484950586eacf71e44b9bdbf7f952a081dcd0a Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Wed, 18 Jun 2025 12:06:17 +0200 Subject: [PATCH 06/20] fixes issue with parsing of vm guest data #448 --- module/sources/vmware/connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 497f04f..02a1765 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -2188,11 +2188,14 @@ def add_virtual_machine(self, obj): if isinstance(detailed_data, str): detailed_data_dict = dict() for detailed_data_item in quoted_split(detailed_data.replace("' ", "', ")): + if "=" not in detailed_data_item: + continue + detailed_data_key, detailed_data_value = detailed_data_item.split("=") detailed_data_dict[detailed_data_key] = detailed_data_value.strip("'") if len(detailed_data_dict.get("prettyName","")) > 0: platform = detailed_data_dict.get("prettyName") - if detailed_data_dict.get("prettyName").lower() == "linux" and \ + if detailed_data_dict.get("familyName", "").lower() == "linux" and \ detailed_data_dict.get("distroVersion") not in platform: platform = f'{platform} {detailed_data_dict.get("distroVersion")}' From 328c70b9f1603e49f4682c3c5666094841db0a4f Mon Sep 17 00:00:00 2001 From: joachimBurket Date: Thu, 3 Jul 2025 17:41:43 +0200 Subject: [PATCH 07/20] only set the Cluster scope_type if the site_name is not None --- module/sources/vmware/connection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 9ab8974..4d17d5e 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -1388,8 +1388,9 @@ def add_cluster(self, obj): } if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): - data["scope_id"] = {"name": site_name} - data["scope_type"] = "dcim.site" + if site_name is not None: + data["scope_id"] = {"name": site_name} + data["scope_type"] = "dcim.site" else: data["site"] = {"name": site_name} From 1320d8105cb8b4eb86391bbac33466200033542f Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Wed, 30 Jul 2025 09:27:08 +0200 Subject: [PATCH 08/20] adds note to readme about sunsetting this project #474 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0430a67..b944ac2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # NetBox-Sync +> [!CAUTION] +> **Maintainer wanted - sunsetting this repository by 31.10.2025 [#474](https://github.com/bb-Ricardo/netbox-sync/issues/474)** + This is a tool to sync data from different sources to a NetBox instance. Available source types: From 71a8dbf4d9947a0afe7bfc564199f9330e89e1d1 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 21 Aug 2025 13:27:53 +1000 Subject: [PATCH 09/20] cluster site now optional, added scope type option. incomplete --- module/netbox/object_classes.py | 80 +++++++++++-------- module/sources/vmware/config.py | 18 +++++ module/sources/vmware/connection.py | 120 ++++++++++++++++++++++++++-- 3 files changed, 177 insertions(+), 41 deletions(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index 9d25093..d37a107 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -1272,6 +1272,20 @@ def get_site_name(self, data=None): if isinstance(this_site, dict): return this_site.get("name") + + # def get_scope_type(self, data=None): + # this_data_set = data + # if this_data_set is None: + # this_data_set = self.data + + # return this_data_set.get("scope_type") + + # def get_scope_id(self, data=None): + # this_data_set = data + # if this_data_set is None: + # this_data_set = self.data + + # return this_data_set.get("scope_id") class NBObjectList(list): @@ -1424,39 +1438,39 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -# class NBLocation(NetBoxObject): -# name = "location" -# api_path = "dcim/locations" -# object_type = "dcim.location" -# primary_key = "name" -# prune = False -# read_only = True -# -# def __init__(self, *args, **kwargs): -# self.data_model = { -# "name": 100, -# "slug": 100, -# "site": NBSite, -# "tags": NBTagList -# } -# super().__init__(*args, **kwargs) -# -# -# class NBRegion(NetBoxObject): -# name = "region" -# api_path = "dcim/regions" -# object_type = "dcim.region" -# primary_key = "name" -# prune = False -# read_only = True -# -# def __init__(self, *args, **kwargs): -# self.data_model = { -# "name": 100, -# "slug": 100, -# "tags": NBTagList -# } -# super().__init__(*args, **kwargs) +class NBLocation(NetBoxObject): + name = "location" + api_path = "dcim/locations" + object_type = "dcim.location" + primary_key = "name" + prune = False + read_only = True + + def __init__(self, *args, **kwargs): + self.data_model = { + "name": 100, + "slug": 100, + "site": NBSite, + "tags": NBTagList + } + super().__init__(*args, **kwargs) + + +class NBRegion(NetBoxObject): + name = "region" + api_path = "dcim/regions" + object_type = "dcim.region" + primary_key = "name" + prune = False + read_only = True + + def __init__(self, *args, **kwargs): + self.data_model = { + "name": 100, + "slug": 100, + "tags": NBTagList + } + super().__init__(*args, **kwargs) class NBSite(NetBoxObject): diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index 7e448a2..2ba7e75 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -143,6 +143,24 @@ def __init__(self): description="""Same as cluster site but on host level. If unset it will fall back to cluster_site_relation""", config_example="nyc02.* = New York, ffm01.* = Frankfurt"), + ConfigOption("cluster_scope_type_relation", + str, + description="""This option defines the scope type for a cluster. + The scope type can be 'site', 'site-group', 'location' or 'region'. + This is done with a comma separated key = value list. + key: defines a cluster name as regex + value: defines the NetBox scope type name (use quotes if name contains commas) + """, + config_example="Cluster_NYC = site, Cluster_FFM = sitegroup, Cluster_BER = location"), + ConfigOption("cluster_scope_id_relation", + str, + description="""This option defines the scope id for a cluster. + The scope id is the NetBox ID of the scope type. + This is done with a comma separated key = value list. + key: defines a cluster name as regex + value: defines the NetBox scope id (use quotes if name contains commas) + """, + config_example="Cluster_NYC = New York, Cluster_FFM.* = Data Centers, Cluster_BER = Building 1"), ConfigOption("cluster_tenant_relation", str, description="""\ diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 742f54c..d70a376 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -478,11 +478,14 @@ def get_site_name(self, object_type, object_name, cluster_name=""): site_name = self.get_site_name(NBCluster, cluster_name) if site_name is not None: log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'") + else: + site_name = self.site_name + log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") # set default site name - if site_name is None: - site_name = self.site_name - log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'") + # if site_name is None: + # site_name = self.site_name + # log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'") # set the site for cluster to None if None-keyword ("") is set via cluster_site_relation if object_type == NBCluster and site_name == "": @@ -490,7 +493,94 @@ def get_site_name(self, object_type, object_name, cluster_name=""): log.debug2(f"Site relation for '{object_name}' set to None") return site_name + + def get_scope_type(self, object_type, object_name): + """ + Retrieve the scope_type for a NBCluster instance by object name or from the config option + cluster_scope_type_relation + + Note: Only NBCluster is supported as the object_type. + + Parameters + ---------- + object_type: object type + The NetBox object type (must be NBCluster). + object_name: str + The name of the object to look up. + + Returns + ------- + str or None: scope type if one is found, otherwise None + """ + + # Validate object type + if object_type != NBCluster: + raise ValueError(f"Object type must be '{NBCluster.name}'.") + + relation_name = "cluster_scope_type_relation" + scope_type = self.get_object_relation(object_name, relation_name) + + object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) + + if object_instance is None: + log.debug2(f"No {object_type.name} found with name '{object_name}'.") + return None + + if scope_type is None: + scope_type = object_instance.data_model.get("scope_type") + if scope_type is None: + log.debug2(f"No scope type found for {object_instance.get_display_name()}.") + return None + + if type(scope_type) is not str: + scope_type_list = scope_type + scope_type = scope_type_list[0] if len(scope_type_list) > 0 else None + + return scope_type + + def get_scope_id(self, object_type, object_name): + """ + Retrieve the scope_id for a NBCluster instance by object name or from the config option + cluster_scope_id_relation + Note: Only NBCluster is supported as the object_type. + + Parameters + ---------- + object_type: type + The NetBox object type (must be NBCluster). + object_name: str + The name of the object to look up. + + Returns + ------- + str or None: scope id if one is found, otherwise None + """ + # Validate object type + if object_type != NBCluster: + raise ValueError(f"Object type must be '{NBCluster.name}'.") + + + relation_name = "cluster_scope_type_relation" + + scope_id = self.get_object_relation(object_name, relation_name) + + object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) + + if object_instance is None: + log.debug2(f"No {object_type.name} found with name '{object_name}'.") # changed log level to warning for testing + return None + + if scope_id is None: + scope_id = object_instance.data_model.get("scope_id") + if scope_id is None: + log.debug2(f"No scope id found for {object_instance.get_display_name()}.") # changed log level to warning for testing + return None + if type(scope_id) is not str: + log.debug(f"scope_id is type: {type(scope_id)}, not str") + return None + return scope_id + def get_object_based_on_macs(self, object_type, mac_list=None): """ Try to find a NetBox object based on list of MAC addresses. @@ -1379,7 +1469,14 @@ def add_cluster(self, obj): self.settings.cluster_exclude_filter) is False: return - site_name = self.get_site_name(NBCluster, full_cluster_name) + scope_type = self.get_scope_type(NBCluster, full_cluster_name) + if scope_type is None: + scope_type = self.get_scope_type(NBCluster, name) + if scope_type == "dcim.site": + site_name = self.get_site_name(NBCluster, full_cluster_name) + scope_id = self.get_scope_id(NBCluster, full_cluster_name) + if scope_id is None: + scope_id = self.get_scope_id(NBCluster, name) data = { "name": name, @@ -1388,11 +1485,18 @@ def add_cluster(self, obj): } if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): - if site_name is not None: - data["scope_id"] = {"name": site_name} - data["scope_type"] = "dcim.site" + # four scope types here (dcim.site, dcim.location, dcim.region, dcim.sitegroup) + if scope_type is not None: + data["scope_type"] = scope_type + data["scope_id"] = {"name": scope_id} + log.debug(f"Cluster '{full_cluster_name}' (or {name}) has scope type '{scope_type}' " + f"and scope id '{scope_id}'.") + else: + log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.") else: - data["site"] = {"name": site_name} + # old verison has site only (# TODO: required??) --> optional (tested in netbox versions 4.1.11 and 3.7.1) + if site_name is not None: + data["site"] = {"name": site_name} tenant_name = self.get_object_relation(full_cluster_name, "cluster_tenant_relation") if tenant_name is not None: From c090d83c0e531d9eccbe6b3702e261fbe173f330 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 21 Aug 2025 15:07:01 +1000 Subject: [PATCH 10/20] added debugging logs, for debugging --- module/sources/vmware/connection.py | 50 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index d70a376..5ff6c0d 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -483,15 +483,21 @@ def get_site_name(self, object_type, object_name, cluster_name=""): log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") # set default site name - # if site_name is None: - # site_name = self.site_name - # log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'") + if site_name is None and object_type == NBDevice: + site_name = self.site_name + log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'") # set the site for cluster to None if None-keyword ("") is set via cluster_site_relation if object_type == NBCluster and site_name == "": site_name = None log.debug2(f"Site relation for '{object_name}' set to None") + if site_name is None and object_type == NBCluster: + log.debug(f"No site relation for {object_type.name} '{object_name}' found, using default site '{self.site_name}'") + site_name = self.site_name + + log.debug(f"Returning site name '{site_name}' for {object_type.name} '{object_name}'. End of method.") + return site_name def get_scope_type(self, object_type, object_name): @@ -519,23 +525,30 @@ def get_scope_type(self, object_type, object_name): relation_name = "cluster_scope_type_relation" scope_type = self.get_object_relation(object_name, relation_name) + log.debug(f"Retrieved scope type '{scope_type}' for {object_type.name} '{object_name}' from relation '{relation_name}'.") - object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) + # object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) + # log.debug(f"Retrieved object instance for {object_type.name} '{object_name}'") - if object_instance is None: - log.debug2(f"No {object_type.name} found with name '{object_name}'.") - return None + # if object_instance is None: + # log.debug(f"No {object_type.name} found with name '{object_name}'.") + # return None - if scope_type is None: - scope_type = object_instance.data_model.get("scope_type") - if scope_type is None: - log.debug2(f"No scope type found for {object_instance.get_display_name()}.") - return None + # if scope_type is None: + # scope_type = object_instance.data_model.get("scope_type") + # if scope_type is None: + # log.debug(f"No scope type found for {object_name}.") + # return None - if type(scope_type) is not str: + if scope_type is not None and type(scope_type) is list: scope_type_list = scope_type scope_type = scope_type_list[0] if len(scope_type_list) > 0 else None + log.debug(f"Scope type for {object_type.name} '{object_name}' is a list, using first element: '{scope_type}'") + if type(scope_type) is not str: + log.debug(f"scope_type is type: {type(scope_type)}, not str") + return None + log.debug(f"Returning scope type '{scope_type}' for {object_type.name} '{object_name}'. End of method.") return scope_type def get_scope_id(self, object_type, object_name): @@ -574,11 +587,14 @@ def get_scope_id(self, object_type, object_name): if scope_id is None: scope_id = object_instance.data_model.get("scope_id") if scope_id is None: - log.debug2(f"No scope id found for {object_instance.get_display_name()}.") # changed log level to warning for testing + log.debug2(f"No scope id found for {object_name}.") # changed log level to warning for testing return None if type(scope_id) is not str: log.debug(f"scope_id is type: {type(scope_id)}, not str") return None + + log.debug(f"Retrieved scope id '{scope_id}' for {object_type.name} '{object_name}' from relation '{relation_name}'. End of method.") + return scope_id def get_object_based_on_macs(self, object_type, mac_list=None): @@ -1468,15 +1484,21 @@ def add_cluster(self, obj): self.settings.cluster_include_filter, self.settings.cluster_exclude_filter) is False: return + log.debug(f"Cluster '{name}' passes include and exclude filters. Continuing.") scope_type = self.get_scope_type(NBCluster, full_cluster_name) + log.debug(f"Cluster '{full_cluster_name}' has scope type '{scope_type}' of type {type(scope_type)}.") if scope_type is None: scope_type = self.get_scope_type(NBCluster, name) + log.debug(f"Cluster '{full_cluster_name}' has scope type '{scope_type}' of type {type(scope_type)}.") if scope_type == "dcim.site": site_name = self.get_site_name(NBCluster, full_cluster_name) + log.debug(f"Cluster '{full_cluster_name}' has site name '{site_name}' of type {type(site_name)}.") scope_id = self.get_scope_id(NBCluster, full_cluster_name) + log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") if scope_id is None: scope_id = self.get_scope_id(NBCluster, name) + log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") data = { "name": name, From 2d773ba4b9bac965b5e88965c016435d199568f9 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 21 Aug 2025 16:11:24 +1000 Subject: [PATCH 11/20] fix a rather important typo for this bug fix --- module/sources/vmware/connection.py | 45 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 5ff6c0d..5b98a52 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -467,20 +467,22 @@ def get_site_name(self, object_type, object_name, cluster_name=""): if object_type not in [NBCluster, NBDevice]: raise ValueError(f"Object must be a '{NBCluster.name}' or '{NBDevice.name}'.") - log.debug2(f"Trying to find site name for {object_type.name} '{object_name}'") + log.debug(f"Trying to find site name for {object_type.name} '{object_name}'") # check if site was provided in config relation_name = "host_site_relation" if object_type == NBDevice else "cluster_site_relation" site_name = self.get_object_relation(object_name, relation_name) - if object_type == NBDevice and site_name is None: + if object_type == NBDevice: # and site_name is None: site_name = self.get_site_name(NBCluster, cluster_name) if site_name is not None: log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'") else: - site_name = self.site_name - log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") + site_name = self.get_object_relation(object_name, relation_name) + if site_name is None: + site_name = self.site_name + log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") # set default site name if site_name is None and object_type == NBDevice: @@ -493,8 +495,7 @@ def get_site_name(self, object_type, object_name, cluster_name=""): log.debug2(f"Site relation for '{object_name}' set to None") if site_name is None and object_type == NBCluster: - log.debug(f"No site relation for {object_type.name} '{object_name}' found, using default site '{self.site_name}'") - site_name = self.site_name + log.debug(f"No site relation for {object_type.name} '{object_name}' found") log.debug(f"Returning site name '{site_name}' for {object_type.name} '{object_name}'. End of method.") @@ -574,20 +575,20 @@ def get_scope_id(self, object_type, object_name): raise ValueError(f"Object type must be '{NBCluster.name}'.") - relation_name = "cluster_scope_type_relation" + relation_name = "cluster_scope_id_relation" scope_id = self.get_object_relation(object_name, relation_name) - object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) + # object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) - if object_instance is None: - log.debug2(f"No {object_type.name} found with name '{object_name}'.") # changed log level to warning for testing - return None + # if object_instance is None: + # log.debug2(f"No {object_type.name} found with name '{object_name}'.") + # return None if scope_id is None: - scope_id = object_instance.data_model.get("scope_id") + scope_id = object_name if scope_id is None: - log.debug2(f"No scope id found for {object_name}.") # changed log level to warning for testing + log.debug(f"No scope id found for {object_name}.") return None if type(scope_id) is not str: log.debug(f"scope_id is type: {type(scope_id)}, not str") @@ -1491,11 +1492,12 @@ def add_cluster(self, obj): if scope_type is None: scope_type = self.get_scope_type(NBCluster, name) log.debug(f"Cluster '{full_cluster_name}' has scope type '{scope_type}' of type {type(scope_type)}.") - if scope_type == "dcim.site": - site_name = self.get_site_name(NBCluster, full_cluster_name) - log.debug(f"Cluster '{full_cluster_name}' has site name '{site_name}' of type {type(site_name)}.") + site_name = self.get_site_name(NBCluster, full_cluster_name) + log.debug(f"Cluster '{full_cluster_name}' has site name '{site_name}' of type {type(site_name)}.") + scope_id = self.get_scope_id(NBCluster, full_cluster_name) log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") + if scope_id is None: scope_id = self.get_scope_id(NBCluster, name) log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") @@ -1537,11 +1539,12 @@ def add_cluster(self, obj): if grab(cluster_candidate, "data.name") != name: continue - # try to find a cluster with matching site - if cluster_candidate.get_site_name() == site_name: - cluster_object = cluster_candidate - log.debug2("Found an existing cluster where 'name' and 'site' are matching") - break + if site_name is not None: + # try to find a cluster with matching site + if cluster_candidate.get_site_name() == site_name: + cluster_object = cluster_candidate + log.debug2("Found an existing cluster where 'name' and 'site' are matching") + break if grab(cluster_candidate, "data.group") is not None and \ grab(cluster_candidate, "data.group.data.name") == group_name: From 9b86ccb301296c9fd4a94bcc778f8b1b06b58775 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 21 Aug 2025 16:38:39 +1000 Subject: [PATCH 12/20] added the site_name as a way to set the scope_id and scope_type (as a site) --- module/sources/vmware/connection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 5b98a52..b28c901 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -1515,6 +1515,9 @@ def add_cluster(self, obj): data["scope_id"] = {"name": scope_id} log.debug(f"Cluster '{full_cluster_name}' (or {name}) has scope type '{scope_type}' " f"and scope id '{scope_id}'.") + elif site_name is not None: + data["scope_type"] = "dcim.site" + data["scope_id"] = {"name": site_name} else: log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.") else: From 61163020390a078ca4966e71560cf744d3f55ea1 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 21 Aug 2025 16:58:45 +1000 Subject: [PATCH 13/20] updated the config option descriptions (written to generated config files) --- module/sources/vmware/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index 2ba7e75..c663f2f 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -146,12 +146,12 @@ def __init__(self): ConfigOption("cluster_scope_type_relation", str, description="""This option defines the scope type for a cluster. - The scope type can be 'site', 'site-group', 'location' or 'region'. + The scope type can be 'dcim.site', 'dcim.sitegroup', 'dcim.location' or 'dcim.region'. This is done with a comma separated key = value list. key: defines a cluster name as regex value: defines the NetBox scope type name (use quotes if name contains commas) """, - config_example="Cluster_NYC = site, Cluster_FFM = sitegroup, Cluster_BER = location"), + config_example="Cluster_NYC = dcim.site, Cluster_FFM = dcim.sitegroup, Cluster_BER = dcim.location"), ConfigOption("cluster_scope_id_relation", str, description="""This option defines the scope id for a cluster. From 453f0e5cb5147425daa19129ab54314286179c42 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 09:43:09 +1000 Subject: [PATCH 14/20] updated some comments, uncommented some methods. Need to find where scope_type is being set --- module/netbox/object_classes.py | 20 ++++++++++---------- module/sources/vmware/connection.py | 19 ++++--------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index d37a107..d405dde 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -1273,19 +1273,19 @@ def get_site_name(self, data=None): if isinstance(this_site, dict): return this_site.get("name") - # def get_scope_type(self, data=None): - # this_data_set = data - # if this_data_set is None: - # this_data_set = self.data + def get_scope_type(self, data=None): + this_data_set = data + if this_data_set is None: + this_data_set = self.data - # return this_data_set.get("scope_type") + return this_data_set.get("scope_type") - # def get_scope_id(self, data=None): - # this_data_set = data - # if this_data_set is None: - # this_data_set = self.data + def get_scope_id(self, data=None): + this_data_set = data + if this_data_set is None: + this_data_set = self.data - # return this_data_set.get("scope_id") + return this_data_set.get("scope_id") class NBObjectList(list): diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index b28c901..df0c3f6 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -474,12 +474,14 @@ def get_site_name(self, object_type, object_name, cluster_name=""): site_name = self.get_object_relation(object_name, relation_name) - if object_type == NBDevice: # and site_name is None: + # check if cluster is in a different site than the host and override the site name if so + if object_type == NBDevice: site_name = self.get_site_name(NBCluster, cluster_name) if site_name is not None: - log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'") + log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'. Overriding host site relation '{relation_name}'") else: site_name = self.get_object_relation(object_name, relation_name) + # set deault site name if no relation was found if site_name is None: site_name = self.site_name log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") @@ -528,19 +530,6 @@ def get_scope_type(self, object_type, object_name): scope_type = self.get_object_relation(object_name, relation_name) log.debug(f"Retrieved scope type '{scope_type}' for {object_type.name} '{object_name}' from relation '{relation_name}'.") - # object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) - # log.debug(f"Retrieved object instance for {object_type.name} '{object_name}'") - - # if object_instance is None: - # log.debug(f"No {object_type.name} found with name '{object_name}'.") - # return None - - # if scope_type is None: - # scope_type = object_instance.data_model.get("scope_type") - # if scope_type is None: - # log.debug(f"No scope type found for {object_name}.") - # return None - if scope_type is not None and type(scope_type) is list: scope_type_list = scope_type scope_type = scope_type_list[0] if len(scope_type_list) > 0 else None From a20bd47e5d380ee8a467f5baf8980bd4bba0a935 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 13:24:15 +1000 Subject: [PATCH 15/20] updated NBCluster class to allow different scope types --- module/netbox/object_classes.py | 27 ++++++++++++++------------- module/sources/vmware/connection.py | 18 +++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index d405dde..fb8d462 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -552,7 +552,6 @@ def update(self, data=None, read_from_netbox=False, source=None): parsed_data = dict() for key, value in data.items(): - if key not in self.data_model.keys(): log.error(f"Found undefined data model key '{key}' for object '{self.__class__.__name__}'") continue @@ -660,6 +659,7 @@ def update(self, data=None, read_from_netbox=False, source=None): max_len=self.data_model.get("slug")) # update all data items + log.debug2(f"Updating {self.name} '{display_name}' with data: {parsed_data.items()}") data_updated = False for key, new_value in parsed_data.items(): @@ -751,7 +751,8 @@ def update(self, data=None, read_from_netbox=False, source=None): new_value_str = new_value_str.replace("\n", " ") log.info(f"{self.name.capitalize()} '{display_name}' attribute '{key}' changed from " f"'{current_value_str}' to '{new_value_str}'") - + log.debug2(f"Updating {self.name} '{display_name}' attribute '{key}' from " + f"'{current_value_str}' to '{new_value_str}'") self.data[key] = new_value self.updated_items.append(key) data_updated = True @@ -1882,7 +1883,7 @@ class NBCluster(NetBoxObject): api_path = "virtualization/clusters" object_type = "virtualization.cluster" primary_key = "name" - secondary_key = "site" + secondary_key = "scope_id" prune = False # include_secondary_key_if_present = True @@ -1899,7 +1900,7 @@ def __init__(self, *args, **kwargs): "group": NBClusterGroup, "scope_type": self.mapping.scopes_object_types(self.scopes), # currently only site is supported as a scope - "scope_id": NBSite, + "scope_id": NetBoxObject, "tags": NBTagList } super().__init__(*args, **kwargs) @@ -1907,20 +1908,20 @@ def __init__(self, *args, **kwargs): def update(self, data=None, read_from_netbox=False, source=None): # Add adaption for change in NetBox 4.2.0 Device model - if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): - if data.get("site") is not None: - data["scope_id"] = data.get("site") - data["scope_type"] = "dcim.site" - del data["site"] + # if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): + # if data.get("site") is not None: + # data["scope_id"] = data.get("site") + # data["scope_type"] = "dcim.site" + # del data["site"] - if data.get("scope_id") is not None: - data["scope_type"] = "dcim.site" + # if data.get("scope_id") is not None: + # data["scope_type"] = "dcim.site" super().update(data=data, read_from_netbox=read_from_netbox, source=source) def resolve_relations(self): - - self.resolve_scoped_relations("scope_id", "scope_type") + log.debug2(f"Resolving relations for {self.name} '{self.get_display_name()}'") + # self.resolve_scoped_relations("scope_id", "scope_type") super().resolve_relations() diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index df0c3f6..dab63a7 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -484,9 +484,9 @@ def get_site_name(self, object_type, object_name, cluster_name=""): # set deault site name if no relation was found if site_name is None: site_name = self.site_name - log.debug(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") + log.debug2(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'") - # set default site name + # set default site name for devices if site_name is None and object_type == NBDevice: site_name = self.site_name log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'") @@ -1477,16 +1477,12 @@ def add_cluster(self, obj): log.debug(f"Cluster '{name}' passes include and exclude filters. Continuing.") scope_type = self.get_scope_type(NBCluster, full_cluster_name) - log.debug(f"Cluster '{full_cluster_name}' has scope type '{scope_type}' of type {type(scope_type)}.") if scope_type is None: scope_type = self.get_scope_type(NBCluster, name) - log.debug(f"Cluster '{full_cluster_name}' has scope type '{scope_type}' of type {type(scope_type)}.") + site_name = self.get_site_name(NBCluster, full_cluster_name) - log.debug(f"Cluster '{full_cluster_name}' has site name '{site_name}' of type {type(site_name)}.") - scope_id = self.get_scope_id(NBCluster, full_cluster_name) - log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") - + scope_id = self.get_scope_id(NBCluster, full_cluster_name) if scope_id is None: scope_id = self.get_scope_id(NBCluster, name) log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.") @@ -1501,7 +1497,7 @@ def add_cluster(self, obj): # four scope types here (dcim.site, dcim.location, dcim.region, dcim.sitegroup) if scope_type is not None: data["scope_type"] = scope_type - data["scope_id"] = {"name": scope_id} + data["scope_id"] = scope_id log.debug(f"Cluster '{full_cluster_name}' (or {name}) has scope type '{scope_type}' " f"and scope id '{scope_id}'.") elif site_name is not None: @@ -1514,6 +1510,8 @@ def add_cluster(self, obj): if site_name is not None: data["site"] = {"name": site_name} + log.debug(f"Cluster '{full_cluster_name}' (or {name}) has data items '{data.items()}'.") + tenant_name = self.get_object_relation(full_cluster_name, "cluster_tenant_relation") if tenant_name is not None: data["tenant"] = {"name": tenant_name} @@ -1563,8 +1561,10 @@ def add_cluster(self, obj): cluster_object = fallback_cluster_object if cluster_object is not None: + # log.debug(f"1st The data items are {data.items()} for cluster '{name}'") cluster_object.update(data=data, source=self) else: + # log.debug(f"1st (alternative) The data items are {data.items()} for cluster '{name}'") cluster_object = self.inventory.add_update_object(NBCluster, data=data, source=self) self.add_object_to_cache(obj, cluster_object) From 6cd57088debc84cf708d202c1a250f88045310ac Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 14:12:45 +1000 Subject: [PATCH 16/20] re-added site as an NBCluster attribute to maintain backwards compatability --- module/netbox/object_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index fb8d462..b026ff0 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -1901,6 +1901,7 @@ def __init__(self, *args, **kwargs): "scope_type": self.mapping.scopes_object_types(self.scopes), # currently only site is supported as a scope "scope_id": NetBoxObject, + "site": NBSite, "tags": NBTagList } super().__init__(*args, **kwargs) From ac5c33420585efad338efeae617051dbb9e320ff Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 15:15:03 +1000 Subject: [PATCH 17/20] Added dcim.location and dcim.region as valid options for scope type in config --- module/netbox/__init__.py | 2 ++ module/netbox/object_classes.py | 6 ++---- module/sources/vmware/connection.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/module/netbox/__init__.py b/module/netbox/__init__.py index 6891c37..4ef3751 100644 --- a/module/netbox/__init__.py +++ b/module/netbox/__init__.py @@ -17,6 +17,8 @@ NBTenant, NBSite, NBSiteGroup, + NBRegion, + NBLocation, NBVRF, NBVLAN, NBVLANList, diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index b026ff0..635a714 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -563,7 +563,6 @@ def update(self, data=None, read_from_netbox=False, source=None): # check data model to see how we have to parse the value defined_value_type = self.data_model.get(key) - # value must be a string witch a certain max length if isinstance(defined_value_type, int): if not isinstance(value, str): @@ -587,7 +586,7 @@ def update(self, data=None, read_from_netbox=False, source=None): # check if value is in defined list elif value not in defined_value_type: - log.error(f"Invalid data type for '{key}' (must be one of {defined_value_type}), got: '{value}'") + log.error(f"this one. Invalid data type for '{key}' (must be one of {defined_value_type}), got: '{value}'") continue # just check the type of the value @@ -1890,7 +1889,7 @@ class NBCluster(NetBoxObject): def __init__(self, *args, **kwargs): self.mapping = NetBoxMappings() self.scopes = [ - NBSite, NBSiteGroup + NBSite, NBSiteGroup, NBLocation, NBRegion ] self.data_model = { "name": 100, @@ -1899,7 +1898,6 @@ def __init__(self, *args, **kwargs): "tenant": NBTenant, "group": NBClusterGroup, "scope_type": self.mapping.scopes_object_types(self.scopes), - # currently only site is supported as a scope "scope_id": NetBoxObject, "site": NBSite, "tags": NBTagList diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index dab63a7..e1b81a7 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -62,6 +62,8 @@ class VMWareHandler(SourceBase): NBDeviceRole, NBSite, NBSiteGroup, + NBLocation, + NBRegion, NBCluster, NBDevice, NBVM, From 0625c3c2d96161ed84762d8aec6830123980a7fc Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 15:36:59 +1000 Subject: [PATCH 18/20] added as an option for scope type in config --- module/sources/vmware/config.py | 2 ++ module/sources/vmware/connection.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index c663f2f..c884df6 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -148,6 +148,7 @@ def __init__(self): description="""This option defines the scope type for a cluster. The scope type can be 'dcim.site', 'dcim.sitegroup', 'dcim.location' or 'dcim.region'. This is done with a comma separated key = value list. + Can be set to "" to not assign a scope type. key: defines a cluster name as regex value: defines the NetBox scope type name (use quotes if name contains commas) """, @@ -157,6 +158,7 @@ def __init__(self): description="""This option defines the scope id for a cluster. The scope id is the NetBox ID of the scope type. This is done with a comma separated key = value list. + To be used in combination with the 'cluster_scope_type_relation'. key: defines a cluster name as regex value: defines the NetBox scope id (use quotes if name contains commas) """, diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index e1b81a7..7ea2d57 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -540,6 +540,11 @@ def get_scope_type(self, object_type, object_name): if type(scope_type) is not str: log.debug(f"scope_type is type: {type(scope_type)}, not str") return None + + if scope_type == "": + log.debug(f"Scope type for {object_type.name} '{object_name}' is set to None") + return None + log.debug(f"Returning scope type '{scope_type}' for {object_type.name} '{object_name}'. End of method.") return scope_type From 9a70da1c12e223dc43867a238fafb68599508db0 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 16:28:53 +1000 Subject: [PATCH 19/20] cleaned up a bit --- module/netbox/object_classes.py | 30 ++--------------------------- module/sources/vmware/config.py | 1 + module/sources/vmware/connection.py | 17 +++------------- 3 files changed, 6 insertions(+), 42 deletions(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index 635a714..7121d8f 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -563,6 +563,7 @@ def update(self, data=None, read_from_netbox=False, source=None): # check data model to see how we have to parse the value defined_value_type = self.data_model.get(key) + # value must be a string witch a certain max length if isinstance(defined_value_type, int): if not isinstance(value, str): @@ -586,7 +587,7 @@ def update(self, data=None, read_from_netbox=False, source=None): # check if value is in defined list elif value not in defined_value_type: - log.error(f"this one. Invalid data type for '{key}' (must be one of {defined_value_type}), got: '{value}'") + log.error(f"Invalid data type for '{key}' (must be one of {defined_value_type}), got: '{value}'") continue # just check the type of the value @@ -1272,21 +1273,6 @@ def get_site_name(self, data=None): if isinstance(this_site, dict): return this_site.get("name") - - def get_scope_type(self, data=None): - this_data_set = data - if this_data_set is None: - this_data_set = self.data - - return this_data_set.get("scope_type") - - def get_scope_id(self, data=None): - this_data_set = data - if this_data_set is None: - this_data_set = self.data - - return this_data_set.get("scope_id") - class NBObjectList(list): """ @@ -1884,7 +1870,6 @@ class NBCluster(NetBoxObject): primary_key = "name" secondary_key = "scope_id" prune = False - # include_secondary_key_if_present = True def __init__(self, *args, **kwargs): self.mapping = NetBoxMappings() @@ -1906,21 +1891,10 @@ def __init__(self, *args, **kwargs): def update(self, data=None, read_from_netbox=False, source=None): - # Add adaption for change in NetBox 4.2.0 Device model - # if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): - # if data.get("site") is not None: - # data["scope_id"] = data.get("site") - # data["scope_type"] = "dcim.site" - # del data["site"] - - # if data.get("scope_id") is not None: - # data["scope_type"] = "dcim.site" - super().update(data=data, read_from_netbox=read_from_netbox, source=source) def resolve_relations(self): log.debug2(f"Resolving relations for {self.name} '{self.get_display_name()}'") - # self.resolve_scoped_relations("scope_id", "scope_type") super().resolve_relations() diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index c884df6..a8cac9c 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -149,6 +149,7 @@ def __init__(self): The scope type can be 'dcim.site', 'dcim.sitegroup', 'dcim.location' or 'dcim.region'. This is done with a comma separated key = value list. Can be set to "" to not assign a scope type. + Note: this does not remove scope types from existing clusters in NetBox. key: defines a cluster name as regex value: defines the NetBox scope type name (use quotes if name contains commas) """, diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 7ea2d57..c8c25a3 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -498,10 +498,7 @@ def get_site_name(self, object_type, object_name, cluster_name=""): site_name = None log.debug2(f"Site relation for '{object_name}' set to None") - if site_name is None and object_type == NBCluster: - log.debug(f"No site relation for {object_type.name} '{object_name}' found") - - log.debug(f"Returning site name '{site_name}' for {object_type.name} '{object_name}'. End of method.") + log.debug2(f"Returning site name '{site_name}' for {object_type.name} '{object_name}'.") return site_name @@ -574,12 +571,6 @@ def get_scope_id(self, object_type, object_name): relation_name = "cluster_scope_id_relation" scope_id = self.get_object_relation(object_name, relation_name) - - # object_instance = self.inventory.get_by_data(object_type, data={"name": object_name}) - - # if object_instance is None: - # log.debug2(f"No {object_type.name} found with name '{object_name}'.") - # return None if scope_id is None: scope_id = object_name @@ -1501,7 +1492,7 @@ def add_cluster(self, obj): } if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): - # four scope types here (dcim.site, dcim.location, dcim.region, dcim.sitegroup) + # set the scope type and id if they are defined if scope_type is not None: data["scope_type"] = scope_type data["scope_id"] = scope_id @@ -1513,7 +1504,7 @@ def add_cluster(self, obj): else: log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.") else: - # old verison has site only (# TODO: required??) --> optional (tested in netbox versions 4.1.11 and 3.7.1) + # set site_name in the pre-4.2.0 NetBox versions is one is found if site_name is not None: data["site"] = {"name": site_name} @@ -1568,10 +1559,8 @@ def add_cluster(self, obj): cluster_object = fallback_cluster_object if cluster_object is not None: - # log.debug(f"1st The data items are {data.items()} for cluster '{name}'") cluster_object.update(data=data, source=self) else: - # log.debug(f"1st (alternative) The data items are {data.items()} for cluster '{name}'") cluster_object = self.inventory.add_update_object(NBCluster, data=data, source=self) self.add_object_to_cache(obj, cluster_object) From 2c7c3eb59bc9fd918b142482e97c60b05bfb2b41 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 22 Aug 2025 16:49:21 +1000 Subject: [PATCH 20/20] cleaned up some more --- module/netbox/object_classes.py | 7 ++++--- module/sources/vmware/config.py | 2 +- module/sources/vmware/connection.py | 25 ++++++++++++++----------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index 7121d8f..6aa6e4e 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -659,7 +659,6 @@ def update(self, data=None, read_from_netbox=False, source=None): max_len=self.data_model.get("slug")) # update all data items - log.debug2(f"Updating {self.name} '{display_name}' with data: {parsed_data.items()}") data_updated = False for key, new_value in parsed_data.items(): @@ -751,8 +750,7 @@ def update(self, data=None, read_from_netbox=False, source=None): new_value_str = new_value_str.replace("\n", " ") log.info(f"{self.name.capitalize()} '{display_name}' attribute '{key}' changed from " f"'{current_value_str}' to '{new_value_str}'") - log.debug2(f"Updating {self.name} '{display_name}' attribute '{key}' from " - f"'{current_value_str}' to '{new_value_str}'") + self.data[key] = new_value self.updated_items.append(key) data_updated = True @@ -1873,6 +1871,7 @@ class NBCluster(NetBoxObject): def __init__(self, *args, **kwargs): self.mapping = NetBoxMappings() + # scope types allowed for clusters self.scopes = [ NBSite, NBSiteGroup, NBLocation, NBRegion ] @@ -1883,7 +1882,9 @@ def __init__(self, *args, **kwargs): "tenant": NBTenant, "group": NBClusterGroup, "scope_type": self.mapping.scopes_object_types(self.scopes), + # supports scoped clusters "scope_id": NetBoxObject, + # supports pre4.2.0 clusters with site "site": NBSite, "tags": NBTagList } diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index a8cac9c..5d222e5 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -163,7 +163,7 @@ def __init__(self): key: defines a cluster name as regex value: defines the NetBox scope id (use quotes if name contains commas) """, - config_example="Cluster_NYC = New York, Cluster_FFM.* = Data Centers, Cluster_BER = Building 1"), + config_example="Cluster_NYC = 1, Cluster_FFM.* = 2, Cluster_BER = 7"), ConfigOption("cluster_tenant_relation", str, description="""\ diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index c8c25a3..efd07d0 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -469,7 +469,7 @@ def get_site_name(self, object_type, object_name, cluster_name=""): if object_type not in [NBCluster, NBDevice]: raise ValueError(f"Object must be a '{NBCluster.name}' or '{NBDevice.name}'.") - log.debug(f"Trying to find site name for {object_type.name} '{object_name}'") + log.debug2(f"Trying to find site name for {object_type.name} '{object_name}'") # check if site was provided in config relation_name = "host_site_relation" if object_type == NBDevice else "cluster_site_relation" @@ -525,24 +525,28 @@ def get_scope_type(self, object_type, object_name): if object_type != NBCluster: raise ValueError(f"Object type must be '{NBCluster.name}'.") + # get scope type from relation config relation_name = "cluster_scope_type_relation" scope_type = self.get_object_relation(object_name, relation_name) log.debug(f"Retrieved scope type '{scope_type}' for {object_type.name} '{object_name}' from relation '{relation_name}'.") + # if the scope_type is a list, use the first element if scope_type is not None and type(scope_type) is list: scope_type_list = scope_type scope_type = scope_type_list[0] if len(scope_type_list) > 0 else None log.debug(f"Scope type for {object_type.name} '{object_name}' is a list, using first element: '{scope_type}'") + # if scope_type is not a str, return None if type(scope_type) is not str: log.debug(f"scope_type is type: {type(scope_type)}, not str") return None + # set scope_type to None if it is configured as "" if scope_type == "": log.debug(f"Scope type for {object_type.name} '{object_name}' is set to None") return None - log.debug(f"Returning scope type '{scope_type}' for {object_type.name} '{object_name}'. End of method.") + log.debug2(f"Returning scope type '{scope_type}' for {object_type.name} '{object_name}'.") return scope_type def get_scope_id(self, object_type, object_name): @@ -567,21 +571,19 @@ def get_scope_id(self, object_type, object_name): if object_type != NBCluster: raise ValueError(f"Object type must be '{NBCluster.name}'.") - + # get scope id from relation config relation_name = "cluster_scope_id_relation" - scope_id = self.get_object_relation(object_name, relation_name) + # return None if scope_id is None or not a string if scope_id is None: - scope_id = object_name - if scope_id is None: - log.debug(f"No scope id found for {object_name}.") - return None + log.debug(f"No scope id found for {object_name}.") + return None if type(scope_id) is not str: log.debug(f"scope_id is type: {type(scope_id)}, not str") return None - log.debug(f"Retrieved scope id '{scope_id}' for {object_type.name} '{object_name}' from relation '{relation_name}'. End of method.") + log.debug2(f"Retrieved scope id '{scope_id}' for {object_type.name} '{object_name}' from relation '{relation_name}'. End of method.") return scope_id @@ -1472,8 +1474,9 @@ def add_cluster(self, obj): self.settings.cluster_include_filter, self.settings.cluster_exclude_filter) is False: return - log.debug(f"Cluster '{name}' passes include and exclude filters. Continuing.") + log.debug2(f"Cluster '{name}' passes include and exclude filters. Continuing.") + # get scope type and id, or site name scope_type = self.get_scope_type(NBCluster, full_cluster_name) if scope_type is None: scope_type = self.get_scope_type(NBCluster, name) @@ -1504,7 +1507,7 @@ def add_cluster(self, obj): else: log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.") else: - # set site_name in the pre-4.2.0 NetBox versions is one is found + # set site_name in the pre-4.2.0 NetBox versions if one is found if site_name is not None: data["site"] = {"name": site_name}