Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
92711a9
enhances NetBox version detection #446
bb-Ricardo Mar 20, 2025
720a3ba
fixes issue with retrieving platform name for newer guest tools #448
bb-Ricardo Mar 21, 2025
edf0495
fix: handle object and multi-object custom fields correctly
Littlericket Mar 21, 2025
7c14187
fixes issue with type hints for python <3.10.0 setups #456
bb-Ricardo May 9, 2025
cee3425
adds distroVersion to VM linux platform #448
bb-Ricardo Jun 17, 2025
1548495
fixes issue with parsing of vm guest data #448
bb-Ricardo Jun 18, 2025
328c70b
only set the Cluster scope_type if the site_name is not None
joachimBurket Jul 3, 2025
94745d0
Merge pull request #469 from joachimBurket/fix/resolve-cluster-withou…
bb-Ricardo Jul 23, 2025
20d356c
Merge pull request #452 from Littlericket/bug-451
bb-Ricardo Jul 23, 2025
1320d81
adds note to readme about sunsetting this project #474
bb-Ricardo Jul 30, 2025
b080e44
stops ip addresses assigned to FHRP groups from being reassigned
Noah418 Aug 25, 2025
049655a
worded the log better
Noah418 Aug 25, 2025
5f16691
added skipping_fhrp_group_ips config option to enable or disable fhrp…
Noah418 Aug 25, 2025
960680c
limited skipping_fhrp_group_ips to vmware since I can't test redfish …
Noah418 Aug 25, 2025
dae2911
tweaked comment
Noah418 Aug 25, 2025
21db416
Merge branch 'bb-Ricardo:main' into 445-sync-overwriting-FHRP-Group-a…
Noah418 Aug 25, 2025
b61a34b
limited skip_fhrp_group_ips to vmware since I can't test redfish pres…
Noah418 Aug 25, 2025
b853384
Merge remote-tracking branch 'origin/445-sync-overwriting-FHRP-Group-…
Noah418 Aug 26, 2025
7e0fde9
modified: module/sources/vmware/connection.py
Noah418 Aug 26, 2025
ee80b80
modified: module/sources/vmware/connection.py
Noah418 Aug 26, 2025
0be0669
changed the NBIPAddress data model entry 'assigned_object_type' to ha…
Noah418 Aug 26, 2025
2e3fb45
modified: module/netbox/object_classes.py
Noah418 Aug 26, 2025
6ffa661
modified: module/netbox/object_classes.py
Noah418 Aug 27, 2025
3a22fc2
Undid part of a previous commit, to undo a mistake
Noah418 Aug 28, 2025
3d9a189
Added a dev document for explaining how the code works for the benefi…
Noah418 Aug 28, 2025
3f5af8c
Added/changed where the fhrp group ip address updating prevention occ…
Noah418 Aug 28, 2025
a7acdac
removed the dev doc since it's not related to this bug
Noah418 Aug 28, 2025
11ed756
Removed the last of the unnecessary things (hopefully)
Noah418 Aug 28, 2025
bd3d882
one last tiny thing
Noah418 Aug 28, 2025
409642d
cleanup unnecessary code
Noah418 Aug 29, 2025
1e865b7
removed the readme note not in the dev branch
Noah418 Aug 29, 2025
c04587c
Merge pull request #6 from sol1/445-sync-overwriting-FHRP-Group-assig…
afoster Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions module/netbox/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
25 changes: 24 additions & 1 deletion module/netbox/object_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions module/sources/check_redfish/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]

Expand Down
11 changes: 9 additions & 2 deletions module/sources/common/source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# repository or visit: <https://opensource.org/licenses/MIT>.

import re
from typing import Union,Optional

from ipaddress import ip_interface, ip_address, IPv6Address, IPv4Address, IPv6Network, IPv4Network
from packaging import version
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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="")
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions module/sources/vmware/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 21 additions & 8 deletions module/sources/vmware/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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)
Expand Down