Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 18 additions & 0 deletions module/sources/common/source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,26 @@ def add_update_interface(self, interface_object, device_object, interface_data,
# try to find matching IP address object
this_ip_object = None
skip_this_ip = False

ip_is_overlapping = any(
ip_object.ip in subnet
for subnet in grab(self.settings, "vm_ip_permitted_overlapping_subnets", fallback=list())
)

if ip_is_overlapping:
for ip in self.inventory.get_all_items(NBIPAddress):
ip_address_string = grab(ip, "data.address", fallback="")
if not ip_address_string.startswith(f"{ip_object.ip.compressed}/"):
continue
if ip.get_interface() == interface_object:
this_ip_object = ip
break

for ip in self.inventory.get_all_items(NBIPAddress):

if ip_is_overlapping:
continue

# check if address matches (without prefix length)
ip_address_string = grab(ip, "data.address", fallback="")

Expand Down
76 changes: 73 additions & 3 deletions module/sources/vmware/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# repository or visit: <https://opensource.org/licenses/MIT>.

import re
from ipaddress import ip_address
from ipaddress import ip_address, ip_network

from module.common.misc import quoted_split
from module.config import source_config_section_name
Expand Down Expand Up @@ -82,6 +82,18 @@ def __init__(self):

ConfigOption(**config_option_permitted_subnets_definition),

ConfigOption("vm_ip_permitted_overlapping_subnets",
str,
description="""\
Define subnets where the same IP address may legitimately appear on
multiple VM interfaces simultaneously — for example, isolated HA
peer-to-peer links where the same /30 addressing is reused across
many VM pairs. Supply a comma-separated list of prefixes in CIDR
notation. When an IP falls within one of these subnets, netbox-sync
creates a separate NetBox IP address object per interface rather than
sharing a single object across VMs.""",
config_example="10.99.99.0/24, 192.168.200.0/24"),

ConfigOptionGroup(title="filter",
description="""filters can be used to include/exclude certain objects from importing
into NetBox. Include filters are checked first and exclude filters after.
Expand Down Expand Up @@ -166,6 +178,20 @@ def __init__(self):
value: defines the desired NetBox platform name""",
config_example="VMware ESXi 7.0.3 = VMware ESXi 7.0 Update 3o"),
ConfigOption("vm_platform_relation", str, config_example="centos-7.* = centos7, microsoft-windows-server-2016.* = Windows2016"),
ConfigOption("vm_platform_from_annotation_relation",
str,
description="""\
Override the platform of a VM based on the content of its vCenter
annotation (the Notes field, synced to the NetBox comments field).
Useful when vSphere misidentifies the guest OS — for example, F5
BIG-IP/BIG-IQ Virtual Edition VMs report as CentOS but their
annotation contains product-identifying text.
This is done with a comma separated key = value list.
key: regex matched anywhere in the annotation text (re.search,
re.DOTALL — patterns span newlines automatically)
value: defines the desired NetBox platform name
Takes priority over vm_platform_relation when both match.""",
config_example="Virtual Edition.*F5 = TMOS"),
ConfigOption("host_role_relation",
str,
description="""\
Expand Down Expand Up @@ -469,6 +495,37 @@ def validate_options(self):

continue

if option.key == "vm_platform_from_annotation_relation":

relation_data = list()

for relation in quoted_split(option.value):

object_name = relation.split("=")[0].strip()
relation_name = relation.split("=")[1].strip()

if len(object_name) == 0 or len(relation_name) == 0:
log.error(f"Config option '{relation}' malformed got '{object_name}' for "
f"object name and '{relation_name}' for annotation platform name.")
self.set_validation_failed()
continue

try:
re_compiled = re.compile(object_name, re.DOTALL)
except Exception as e:
log.error(f"Problem parsing regular expression '{object_name}' for '{relation}': {e}")
self.set_validation_failed()
continue

relation_data.append({
"object_regex": re_compiled,
"assigned_name": relation_name
})

option.set_value(relation_data)

continue

if "relation" in option.key and "vlan_group_relation" not in option.key:

relation_data = list()
Expand All @@ -477,8 +534,8 @@ def validate_options(self):

for relation in quoted_split(option.value):

object_name = relation.split("=")[0].strip(' "')
relation_name = relation.split("=")[1].strip(' "')
object_name = relation.split("=")[0].strip()
relation_name = relation.split("=")[1].strip()

if len(object_name) == 0 or len(relation_name) == 0:
log.error(f"Config option '{relation}' malformed got '{object_name}' for "
Expand Down Expand Up @@ -650,3 +707,16 @@ def validate_options(self):
self.set_validation_failed()

permitted_subnets_option.set_value(permitted_subnets)

overlapping_subnets_option = self.get_option_by_name("vm_ip_permitted_overlapping_subnets")

if overlapping_subnets_option is not None and overlapping_subnets_option.value is not None:
subnet_list = [x.strip() for x in overlapping_subnets_option.value.split(",") if x.strip() != ""]
parsed_subnets = []
for subnet in subnet_list:
try:
parsed_subnets.append(ip_network(subnet, strict=False))
except Exception as e:
log.error(f"Problem parsing vm_ip_permitted_overlapping_subnets entry '{subnet}': {e}")
self.set_validation_failed()
overlapping_subnets_option.set_value(parsed_subnets)
16 changes: 12 additions & 4 deletions module/sources/vmware/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2216,9 +2216,17 @@ def add_virtual_machine(self, obj):

hardware_devices = grab(obj, "config.hardware.device", fallback=list())

annotation = None
if self.settings.skip_vm_comments is False:
annotation = get_string_or_none(grab(obj, "config.annotation"))
# always read annotation — needed for platform detection even when skip_vm_comments is True
annotation = get_string_or_none(grab(obj, "config.annotation"))

# override platform based on annotation content; takes priority over vm_platform_relation
if annotation is not None:
for relation in grab(self.settings, "vm_platform_from_annotation_relation", fallback=list()):
if relation.get("object_regex").search(annotation):
platform = relation.get("assigned_name")
log.debug2(f"Overriding VM platform to '{platform}' based on annotation content "
f"(pattern: '{relation.get('object_regex').pattern}')")
break

# assign vm_tenant_relation
tenant_name = self.get_object_relation(name, "vm_tenant_relation")
Expand Down Expand Up @@ -2272,7 +2280,7 @@ def add_virtual_machine(self, obj):

if platform is not None:
vm_data["platform"] = {"name": platform}
if annotation is not None:
if annotation is not None and self.settings.skip_vm_comments is False:
vm_data["comments"] = annotation
if tenant_name is not None:
vm_data["tenant"] = {"name": tenant_name}
Expand Down
5 changes: 5 additions & 0 deletions netbox-sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
Sync objects from various sources to NetBox
"""

import warnings
# vmware-vapi-runtime 2.52.0 imports pkg_resources at runtime, which emits a
# DeprecationWarning on setuptools >= 81. Suppress it until the vapi stack is
# upgraded to 9.x where the import is replaced with importlib.resources.
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)

from datetime import datetime

Expand Down
23 changes: 23 additions & 0 deletions settings-example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ password = super-secret
; blocks a leading '!' has to be added
;permitted_subnets = 172.16.0.0/12, 10.0.0.0/8, 192.168.0.0/16, fd00::/8, !10.23.42.0/24

; Subnets where the same IP address may legitimately appear on multiple VM interfaces
; simultaneously. A common use case is isolated HA peer-to-peer links where the same /30
; addressing is reused across many VM pairs (the IPs are unique within each link VLAN but
; overlap globally). Supply a comma-separated list of prefixes in CIDR notation. When an
; IP falls within one of these subnets, netbox-sync creates a separate NetBox IP address
; object per interface rather than sharing a single object and emitting a duplicate warning.
;vm_ip_permitted_overlapping_subnets = 10.99.99.0/24

; filter options

; filters can be used to include/exclude certain objects from importing into NetBox.
Expand Down Expand Up @@ -217,6 +225,21 @@ password = super-secret
;host_platform_relation = VMware ESXi 7.0.3 = VMware ESXi 7.0 Update 3o
;vm_platform_relation = centos-7.* = centos7, microsoft-windows-server-2016.* = Windows2016

; Override the platform of a VM based on the content of its vCenter annotation (the Notes
; field, synced to the NetBox comments field). Useful when vSphere misidentifies the guest
; OS for appliance-style VMs — for example, network or security virtual appliances that run
; on a modified Linux base are reported by vSphere as the base distro (CentOS, etc.) but
; carry product-identifying text in their annotation.
; Patterns are matched anywhere in the annotation text (re.search) and span newlines
; automatically (re.DOTALL), so multi-line annotations work without special flags.
; Takes priority over vm_platform_relation when both would match.
; This is done with a comma separated key = value list.
; key: defines a regex matched against the full VM annotation content
; value: defines the desired NetBox platform name
;vm_platform_from_annotation_relation =
; BIG-IP Local Traffic Manager Virtual Edition.*F5 = TMOS,
; BIG-IQ Virtual Edition.*F5 = BIG-IQ

; Define the NetBox device role used for hosts. The default is
; set to "Server". This is done with a comma separated key = value list.
; key: defines host(s) name as regex
Expand Down