Skip to content
Merged
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
10 changes: 9 additions & 1 deletion module/netbox/object_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,15 @@ class to handle and determine NetBox interface types
"200gbase-x-qsfp56": 200_000,
"200gbase-x-cfp2": 200_000,
"400gbase-x-qsfpdd": 400_000,
"400gbase-x-osfp": 400_000
"400gbase-x-osfp": 400_000,
"1gfc-sfp": 1,
"2gfc-sfp": 2,
"4gfc-sfp": 4,
"8gfc-sfpp": 8,
"16gfc-sfpp": 16,
"32gfc-sfp28": 32,
"64gfc-qsfpp": 64,
"128gfc-qsfp28": 128
}

# assign common types for an interface speed value
Expand Down
66 changes: 61 additions & 5 deletions module/sources/common/source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ def ip_is_primary_ip_of_object(self, ip_object, device_vm_object) -> bool:

return False

@staticmethod
def is_fc_interface(interface_type, wwn=None, mac_address=None):
"""
Return True if the given interface is a Fibre Channel port.

A Fibre Channel port is identified by a NetBox FC interface type (all FC types
contain 'gfc') or by a world wide name (WWN) without a MAC address. An interface
with both a MAC address and a WWN (a converged network adapter port) is an
Ethernet interface with a WWN and is matched like any other Ethernet interface.
FC ports never take part in the leftover 1:1 name pairing of
'map_object_interfaces_to_current_interfaces'.
"""
return "gfc" in str(interface_type or "") or (wwn is not None and mac_address is None)

def map_object_interfaces_to_current_interfaces(self, device_vm_object, interface_data_dict=None,
append_unmatched_interfaces=False):
"""
Expand All @@ -107,6 +121,14 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
MAC address of interfaces match exactly, distinguish between physical and virtual interfaces
by MAC regardless of interface type
MAC address of interfaces match exactly, type of interface does not matter
by WWN:
an interface with a world wide name matches the current interface with the
same WWN when no interface of that name exists yet

Fibre Channel ports (FC interface type, or a WWN without a MAC address) never take
part in the leftover 1:1 name pairing below: an unmatched FC HBA is created as a
new interface, and an FC current interface is never handed to a discovered
Ethernet NIC. Interfaces with a MAC address are matched as before.

If there are interfaces which don't match at all then the unmatched interfaces will be
matched 1:1. Sort both lists (unmatched current interfaces, unmatched new interfaces)
Expand Down Expand Up @@ -161,6 +183,11 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac

current_object_interface_names = list()

# current interfaces indexed by WWN for WWN matching, and the names of the Fibre
# Channel ports among them, which never take part in the leftover 1:1 name pairing
current_interfaces_by_wwn = dict()
current_fc_interface_names = set()

return_data = dict()

# grab current data
Expand All @@ -172,14 +199,16 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
else:
int_mac = grab(interface, "data.mac_address")
int_name = grab(interface, "data.name")
int_wwn = grab(interface, "data.wwn")
int_type_string = grab(interface, "data.type", fallback="virtual")

if interface_exclude_filter is not None and int_name is not None and \
interface_exclude_filter.match(int_name):
log.debug2(f"Current interface '{int_name}' matches interface_exclude_filter. "
f"Excluding it from all interface matching attempts")
continue
int_type = "virtual"
if "virtual" not in str(grab(interface, "data.type", fallback="virtual")):
if "virtual" not in str(int_type_string):
int_type = "physical"

if int_mac is not None:
Expand All @@ -190,6 +219,13 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
current_object_interfaces[int_name] = interface
current_object_interface_names.append(int_name)

if int_wwn is not None:
current_interfaces_by_wwn[str(int_wwn).upper()] = interface

# keep FC ports out of the leftover pairing
if self.is_fc_interface(int_type_string, int_wwn, int_mac):
current_fc_interface_names.add(int_name)

log.debug2("Found '%d' NICs in NetBox for '%s'" %
(len(current_object_interface_names), device_vm_object.get_display_name()))

Expand All @@ -200,8 +236,12 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
return_data[int_name] = None

int_mac = grab(int_data, "mac_address", fallback="XX:XX:YY:YY:ZZ:ZZ")
int_wwn = grab(int_data, "wwn")
int_type_string = grab(int_data, "type", fallback="virtual")
is_fc = self.is_fc_interface(int_type_string, int_wwn, grab(int_data, "mac_address"))

int_type = "virtual"
if "virtual" not in str(grab(int_data, "type", fallback="virtual")):
if "virtual" not in str(int_type_string):
int_type = "physical"

# match simply by name
Expand All @@ -210,13 +250,20 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
log.debug2(f"Found 1:1 name match for NIC '{int_name}'")
matching_int = current_object_interfaces.get(int_name)

# match by WWN when no interface of that name exists yet
if matching_int is None and int_wwn is not None:
wwn_match = current_interfaces_by_wwn.get(str(int_wwn).upper())
if wwn_match is not None and wwn_match not in return_data.values():
log.debug2(f"Found 1:1 WWN match for interface '{int_name}'")
matching_int = wwn_match

# match mac by interface type
elif grab(current_object_interfaces, f"{int_type}.{int_mac}") is not None:
if matching_int is None and grab(current_object_interfaces, f"{int_type}.{int_mac}") is not None:
log.debug2(f"Found 1:1 MAC address match for {int_type} NIC '{int_name}'")
matching_int = grab(current_object_interfaces, f"{int_type}.{int_mac}")

# match mac regardless of interface type
elif current_object_interfaces.get(int_mac) is not None and \
if matching_int is None and current_object_interfaces.get(int_mac) is not None and \
current_object_interfaces.get(int_mac) not in return_data.values():
log.debug2(f"Found 1:1 MAC address match for NIC '{int_name}' (ignoring interface type)")
matching_int = current_object_interfaces.get(int_mac)
Expand All @@ -228,6 +275,11 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
if grab(matching_int, "data.name") in current_object_interface_names:
current_object_interface_names.remove(grab(matching_int, "data.name"))

# an unmatched FC port is created as a new interface and never takes part in the
# leftover 1:1 name pairing
elif is_fc:
continue

# no match found, we match the leftovers just by #1 -> #1, #2 -> #2, ...
else:
unmatched_interface_names.append(int_name)
Expand All @@ -240,7 +292,11 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac
for int_name in unmatched_interface_names:
return_data[int_name] = None
else:
matching_nics = dict(zip(unmatched_interface_names, current_object_interface_names))
# current FC ports are never handed to a discovered Ethernet NIC by the leftover pairing
leftover_current_interface_names = [name for name in current_object_interface_names
if name not in current_fc_interface_names]

matching_nics = dict(zip(unmatched_interface_names, leftover_current_interface_names))

for new_int, current_int in matching_nics.items():
current_int_object = current_object_interfaces.get(current_int)
Expand Down
13 changes: 13 additions & 0 deletions module/sources/vmware/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,19 @@ def __init__(self):
disabled by default.""",
default_value=False
),
ConfigOption("sync_host_fc_adapters",
bool,
description="""Create NetBox interfaces for the Fibre Channel host bus adapters (HBAs)
of an ESXi host. For each FC HBA an interface is created with the adapter's device
name, an interface type derived from the adapter model's "<n>G"/"<n>Gb" capability
(the first number when several are given) and only falling back to the port speed
when the model gives none, the port WWN and the adapter model and driver as
description. Other host bus adapter types (block, PCIe) are ignored, as is an FC
adapter that reports no port WWN. 'host_interface_exclude_filter' applies to the
adapter names as well. The option is independent of 'skip_host_nics'. Disabled
by default.""",
default_value=False
),

# removed settings
ConfigOption("netbox_host_device_role",
Expand Down
135 changes: 133 additions & 2 deletions module/sources/vmware/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import datetime
import pprint
import re
import ssl
from ipaddress import ip_address, ip_interface
from urllib.parse import unquote
Expand Down Expand Up @@ -111,6 +112,18 @@ class VMWareHandler(SourceBase):
unknown_hardware_identifiers = ["Default string", "NA", "N/A", "None", "Null", "oem", "o.e.m",
"to be filled by o.e.m.", "Unknown", "Generic Vendor", "Generic Model"]

# NetBox interface types for Fibre Channel HBAs, keyed by the adapter port speed in Gbit
fc_hba_interface_types = {
1: "1gfc-sfp",
2: "2gfc-sfp",
4: "4gfc-sfp",
8: "8gfc-sfpp",
16: "16gfc-sfpp",
32: "32gfc-sfp28",
64: "64gfc-qsfpp",
128: "128gfc-qsfp28"
}

def __init__(self, name=None):

if name is None:
Expand Down Expand Up @@ -847,6 +860,75 @@ def hardware_identifier_is_unknown(value):

return value.lower() in [x.lower() for x in VMWareHandler.unknown_hardware_identifiers]

@staticmethod
def fc_hba_interface_type(speed, model=None):
"""
Determine a NetBox interface type for a Fibre Channel host bus adapter.

The adapter model string is used first: the first "<n>G"/"<n>Gb" capability in
the model is taken, i.e. "QLE2700/QLE2800 32/64G SP/DP Fibre Channel Adapter"
yields 32. This keeps the NetBox type stable even when a port loses link and
reports speed 0. Only when the model gives no speed the live port speed reported
by vCenter is used (1, 2, 4, 8, 16, 32, 64 or 128 Gbit). If no speed can be
determined the type "other" is returned.

Parameters
----------
speed: int, str
port speed in Gbit as reported by vCenter
model: str
adapter model string used to derive the speed

Returns
-------
str: a NetBox interface type
"""

if isinstance(model, str):
speed_match = re.search(r"(\d+)(?:/\d+)*Gb?", model)
if speed_match is not None:
model_speed = int(speed_match.group(1))
if model_speed in VMWareHandler.fc_hba_interface_types:
return VMWareHandler.fc_hba_interface_types[model_speed]

try:
speed = int(speed)
except (TypeError, ValueError):
speed = None

if speed in VMWareHandler.fc_hba_interface_types:
return VMWareHandler.fc_hba_interface_types[speed]

return "other"

@staticmethod
def fc_hba_wwn(port_wwn):
"""
Convert a port WWN from the decimal notation vCenter reports to the colon
separated 16 hex digit notation NetBox expects.

Parameters
----------
port_wwn: int, str
port world wide name in decimal notation

Returns
-------
str, None: "xx:xx:xx:xx:xx:xx:xx:xx" or None if no WWN was provided
"""

if port_wwn is None:
return None

try:
port_wwn = int(port_wwn)
except (TypeError, ValueError):
return None

hex_wwn = f"{port_wwn:016x}"

return ":".join(hex_wwn[i:i + 2] for i in range(0, 16, 2)).upper()

def get_site_name(self, object_type, object_name, cluster_name=""):
"""
Return a site name for a NBCluster or NBDevice depending on config options
Expand Down Expand Up @@ -1480,7 +1562,8 @@ def get_object_relation(self, name, relation, fallback=None):
return resolved_name

def add_device_vm_to_inventory(self, object_type, object_data, pnic_data=None, vnic_data=None,
nic_ips=None, p_ipv4=None, p_ipv6=None, vmware_object=None, disk_data=None):
nic_ips=None, p_ipv4=None, p_ipv6=None, vmware_object=None, disk_data=None,
fc_hba_data=None):
"""
Add/update device/VM object in inventory based on gathered data.

Expand Down Expand Up @@ -1543,6 +1626,8 @@ def add_device_vm_to_inventory(self, object_type, object_data, pnic_data=None, v
vmware object to pass on to 'add_update_interface' method to set up reevaluation
disk_data: list
data of discs which belong to a VM
fc_hba_data: dict
data of Fibre Channel host bus adapter interfaces, interface name as key

Returns
-------
Expand Down Expand Up @@ -1741,6 +1826,8 @@ def add_device_vm_to_inventory(self, object_type, object_data, pnic_data=None, v
interface_exclude_filter = self.settings.vm_interface_exclude_filter
else:
nic_data = {**pnic_data, **vnic_data}
if fc_hba_data:
nic_data = {**nic_data, **fc_hba_data}
interface_exclude_filter = self.settings.host_interface_exclude_filter

# exclude discovered interfaces which match the exclude filter
Expand Down Expand Up @@ -2585,6 +2672,49 @@ def add_host(self, obj):

pnic_data_dict[pnic_name] = pnic_data

# collect Fibre Channel host bus adapters as interfaces if enabled
fc_hba_data_dict = dict()
if self.settings.sync_host_fc_adapters is True:
for hba in grab(obj, "config.storageDevice.hostBusAdapter", fallback=list()):

# only Fibre Channel adapters are synced, block and PCIe adapters are ignored
if not isinstance(hba, vim.host.FibreChannelHba):
continue

hba_device = get_string_or_none(grab(hba, "device"))
if hba_device is None:
continue

# the WWN is what tells the interface apart from a NIC during matching
hba_wwn = self.fc_hba_wwn(grab(hba, "portWorldWideName"))
if hba_wwn is None:
log.debug(f"FC HBA '{hba_device}' of host '{name}' reports no port WWN. Skipping")
continue

hba_model = get_string_or_none(grab(hba, "model"))
hba_driver = get_string_or_none(grab(hba, "driver"))

if hba_model is not None and hba_driver is not None:
hba_description = f"{hba_model} ({hba_driver})"
elif hba_model is not None:
hba_description = hba_model
else:
hba_description = hba_driver

fc_hba_data = {
"name": unquote(hba_device),
"device": None, # will be set once we found the correct device
"type": self.fc_hba_interface_type(grab(hba, "speed"), hba_model),
"enabled": get_string_or_none(grab(hba, "status")) == "online",
"mgmt_only": False,
"wwn": hba_wwn
}

if hba_description is not None:
fc_hba_data["description"] = hba_description

fc_hba_data_dict[hba_device] = fc_hba_data

host_primary_ip4 = None
host_primary_ip6 = None

Expand Down Expand Up @@ -2733,7 +2863,8 @@ def add_host(self, obj):
device_object, interface_objects = \
self.add_device_vm_to_inventory(NBDevice, object_data=host_data, pnic_data=pnic_data_dict,
vnic_data=vnic_data_dict, nic_ips=vnic_ips,
p_ipv4=host_primary_ip4, p_ipv6=host_primary_ip6, vmware_object=obj)
p_ipv4=host_primary_ip4, p_ipv6=host_primary_ip6, vmware_object=obj,
fc_hba_data=fc_hba_data_dict)

# add cables to the switch ports which were reported via CDP/LLDP
if device_object is not None:
Expand Down
10 changes: 10 additions & 0 deletions settings-example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,16 @@ password = super-secret
; usually maintained by hand, that's why this is disabled by default.
;sync_host_cables = False

; Create NetBox interfaces for the Fibre Channel host bus adapters (HBAs) of an ESXi host.
; For each FC HBA an interface is created with the adapter's device name, an interface
; type derived from the adapter model's "<n>G"/"<n>Gb" capability (the first number when
; several are given) and only falling back to the port speed when the model gives none,
; the port WWN and the adapter model and driver as description. Other host bus adapter
; types (block, PCIe) are ignored, as is an FC adapter that reports no port WWN.
; 'host_interface_exclude_filter' applies to the adapter names as well. The option is
; independent of 'skip_host_nics'. Disabled by default.
;sync_host_fc_adapters = False

[source/my-redfish-example]

; Defines if this source is enabled or not
Expand Down
Loading
Loading