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}") 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, diff --git a/module/sources/check_redfish/config.py b/module/sources/check_redfish/config.py index 2c21fff..a4f99be 100644 --- a/module/sources/check_redfish/config.py +++ b/module/sources/check_redfish/config.py @@ -70,6 +70,12 @@ def __init__(self): via check_redfish if False only data which is not preset in NetBox will be added""", default_value=False), + ConfigOption("skip_fhrp_group_ips", + bool, + description="""define if an IP address assigned to a FHRP group (like HSRP, VRRP, GLBP) will be skipped. + If True this IP address will be skipped and not synced to NetBox to prevent incorrect syncing.""", + default_value=False), + ConfigOption(**config_option_ip_tenant_inheritance_order_definition), ] diff --git a/module/sources/common/source_base.py b/module/sources/common/source_base.py index fc81a16..7611668 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. @@ -442,6 +443,12 @@ def add_update_interface(self, interface_object, device_object, interface_data, this_ip_object = None skip_this_ip = False for ip in self.inventory.get_all_items(NBIPAddress): + # stops fhrp group assigned ip addresses from being overridden and assigned to another object type + # if the config skip_fhrp_group_ips is set to True + if grab(ip, "data.assigned_object_type", fallback="") == "ipam.fhrpgroup" and self.settings.skip_fhrp_group_ips: + log.info(f"Ip address {grab(ip, "data.address")} is assigned to an FHRP Group and skip_fhrp_group_ips is set to {self.settings.skip_fhrp_group_ips}, skipping.") + skip_this_ip = True + continue # check if address matches (without prefix length) ip_address_string = grab(ip, "data.address", fallback="") @@ -716,7 +723,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. diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index 7e448a2..3e5c731 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -258,6 +258,11 @@ def __init__(self): description="""If the VMware Site Recovery Manager is used to can skip syncing placeholder/replicated VMs from fail-over site to NetBox.""", default_value=False), + ConfigOption("skip_fhrp_group_ips", + bool, + description="""If an IP address is assigned to a FHRP group (like HSRP, VRRP, GLBP) + then this IP address will be skipped and not synced to NetBox to prevent incorrect syncing.""", + default_value=False), ConfigOption("strip_host_domain_name", bool, description="strip domain part from host name before syncing device to NetBox", diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 9ab8974..742f54c 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} @@ -2180,12 +2181,24 @@ 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")] - if len(pretty_name) > 0: - platform = pretty_name[0].replace("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): + 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("familyName", "").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)