mirror of
https://github.com/bb-Ricardo/netbox-sync.git
synced 2026-05-02 15:29:08 -05:00
fa82a33dc9
refs: #91
1118 lines
40 KiB
Python
1118 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2020 - 2021 Ricardo Bartels. All rights reserved.
|
|
#
|
|
# netbox-sync.py
|
|
#
|
|
# This work is licensed under the terms of the MIT license.
|
|
# For a copy, see file LICENSE.txt included in this
|
|
# repository or visit: <https://opensource.org/licenses/MIT>.
|
|
|
|
from ipaddress import ip_network
|
|
import os
|
|
import glob
|
|
import json
|
|
|
|
from packaging import version
|
|
|
|
from module.sources.common.source_base import SourceBase
|
|
from module.common.logging import get_logger
|
|
from module.common.misc import grab, get_string_or_none
|
|
from module.common.support import normalize_mac_address, ip_valid_to_add_to_netbox
|
|
from module.netbox.object_classes import (
|
|
NetBoxInterfaceType,
|
|
NBTag,
|
|
NBManufacturer,
|
|
NBDeviceType,
|
|
NBPlatform,
|
|
NBClusterType,
|
|
NBClusterGroup,
|
|
NBDeviceRole,
|
|
NBSite,
|
|
NBCluster,
|
|
NBDevice,
|
|
NBInterface,
|
|
NBIPAddress,
|
|
NBPrefix,
|
|
NBTenant,
|
|
NBVRF,
|
|
NBVLAN,
|
|
NBPowerPort,
|
|
NBInventoryItem,
|
|
NBCustomField
|
|
)
|
|
|
|
log = get_logger()
|
|
|
|
|
|
class CheckRedfish(SourceBase):
|
|
"""
|
|
Source class to import check_redfish inventory files
|
|
"""
|
|
|
|
# minimum check_redfish inventory version
|
|
minimum_check_redfish_version = "1.2.0"
|
|
|
|
dependent_netbox_objects = [
|
|
NBTag,
|
|
NBManufacturer,
|
|
NBDeviceType,
|
|
NBPlatform,
|
|
NBClusterType,
|
|
NBClusterGroup,
|
|
NBDeviceRole,
|
|
NBSite,
|
|
NBCluster,
|
|
NBDevice,
|
|
NBInterface,
|
|
NBIPAddress,
|
|
NBPrefix,
|
|
NBTenant,
|
|
NBVRF,
|
|
NBVLAN,
|
|
NBPowerPort,
|
|
NBInventoryItem,
|
|
NBCustomField
|
|
]
|
|
|
|
settings = {
|
|
"enabled": True,
|
|
"inventory_file_path": None,
|
|
"permitted_subnets": None,
|
|
"overwrite_host_name": False,
|
|
"overwrite_power_supply_name": False,
|
|
"overwrite_power_supply_attributes": True,
|
|
"overwrite_interface_name": False,
|
|
"overwrite_interface_attributes": True,
|
|
}
|
|
|
|
init_successful = False
|
|
inventory = None
|
|
name = None
|
|
source_tag = None
|
|
source_type = "check_redfish"
|
|
enabled = False
|
|
inventory_file_path = None
|
|
interface_adapter_type_dict = dict()
|
|
device_object = None
|
|
inventory_file_content = None
|
|
|
|
def __init__(self, name=None, settings=None, inventory=None):
|
|
|
|
if name is None:
|
|
raise ValueError(f"Invalid value for attribute 'name': '{name}'.")
|
|
|
|
self.inventory = inventory
|
|
self.name = name
|
|
|
|
self.parse_config_settings(settings)
|
|
|
|
self.source_tag = f"Source: {name}"
|
|
|
|
if self.enabled is False:
|
|
log.info(f"Source '{name}' is currently disabled. Skipping")
|
|
return
|
|
|
|
self.init_successful = True
|
|
|
|
def parse_config_settings(self, config_settings):
|
|
"""
|
|
Validate parsed settings from config file
|
|
|
|
Parameters
|
|
----------
|
|
config_settings: dict
|
|
dict of config settings
|
|
|
|
"""
|
|
|
|
validation_failed = False
|
|
|
|
for setting in ["inventory_file_path"]:
|
|
if config_settings.get(setting) is None:
|
|
log.error(f"Config option '{setting}' in 'source/{self.name}' can't be empty/undefined")
|
|
validation_failed = True
|
|
|
|
inv_path = config_settings.get("inventory_file_path")
|
|
if not os.path.exists(inv_path):
|
|
log.error(f"Inventory file path '{inv_path}' not found.")
|
|
validation_failed = True
|
|
|
|
if os.path.isfile(inv_path):
|
|
log.error(f"Inventory file path '{inv_path}' needs to be a directory.")
|
|
validation_failed = True
|
|
|
|
if not os.access(inv_path, os.X_OK | os.R_OK):
|
|
log.error(f"Inventory file path '{inv_path}' not readable.")
|
|
validation_failed = True
|
|
|
|
# check permitted ip subnets
|
|
if config_settings.get("permitted_subnets") is None:
|
|
log.info(f"Config option 'permitted_subnets' in 'source/{self.name}' is undefined. "
|
|
f"No IP addresses will be populated to Netbox!")
|
|
else:
|
|
config_settings["permitted_subnets"] = \
|
|
[x.strip() for x in config_settings.get("permitted_subnets").split(",") if x.strip() != ""]
|
|
|
|
permitted_subnets = list()
|
|
for permitted_subnet in config_settings["permitted_subnets"]:
|
|
try:
|
|
permitted_subnets.append(ip_network(permitted_subnet))
|
|
except Exception as e:
|
|
log.error(f"Problem parsing permitted subnet: {e}")
|
|
validation_failed = True
|
|
|
|
config_settings["permitted_subnets"] = permitted_subnets
|
|
|
|
if validation_failed is True:
|
|
log.error("Config validation failed. Exit!")
|
|
exit(1)
|
|
|
|
for setting in self.settings.keys():
|
|
setattr(self, setting, config_settings.get(setting))
|
|
|
|
def apply(self):
|
|
"""
|
|
Main source handler method. This method is called for each source from "main" program
|
|
to retrieve data from it source and apply it to the NetBox inventory.
|
|
|
|
Every update of new/existing objects fot this source has to happen here.
|
|
|
|
First try to find and iterate over each inventory file.
|
|
Then parse the system data first and then all components.
|
|
"""
|
|
|
|
# first add all custom fields we need for this source
|
|
self.add_necessary_base_objects()
|
|
|
|
for filename in glob.glob(f"{self.inventory_file_path}/*.json"):
|
|
|
|
self.reset_inventory_state()
|
|
|
|
if self.read_inventory_file_content(filename) is False:
|
|
continue
|
|
|
|
# try to get device by supplied NetBox id
|
|
inventory_id = grab(self.inventory_file_content, "meta.inventory_id")
|
|
self.device_object = self.inventory.get_by_id(NBDevice, inventory_id)
|
|
|
|
# try to find device by serial of first system in inventory
|
|
device_serial = grab(self.inventory_file_content, "inventory.system.0.serial")
|
|
if self.device_object is None:
|
|
self.device_object = self.inventory.get_by_data(NBDevice, data={
|
|
"serial": device_serial
|
|
})
|
|
|
|
if self.device_object is None:
|
|
log.error(f"Unable to find {NBDevice.name} with id '{inventory_id}' or "
|
|
f"serial '{device_serial}' in NetBox inventory from inventory file {filename}")
|
|
continue
|
|
|
|
# parse all components
|
|
self.update_device()
|
|
self.update_power_supply()
|
|
self.update_fan()
|
|
self.update_memory()
|
|
self.update_proc()
|
|
self.update_physical_drive()
|
|
self.update_storage_controller()
|
|
self.update_storage_enclosure()
|
|
self.update_network_adapter()
|
|
self.update_network_interface()
|
|
self.update_manager()
|
|
|
|
def reset_inventory_state(self):
|
|
"""
|
|
reset attributes to make sure not using data from a previous inventory file
|
|
"""
|
|
|
|
self.inventory_file_content = None
|
|
self.device_object = None
|
|
|
|
# reset interface types
|
|
self.interface_adapter_type_dict = dict()
|
|
|
|
def read_inventory_file_content(self, filename: str) -> bool:
|
|
"""
|
|
open an inventory file, parse content to json and compare layout version.
|
|
|
|
Parameters
|
|
----------
|
|
filename: str
|
|
path ot the file to parse
|
|
|
|
Returns
|
|
-------
|
|
success: bool
|
|
True if reading file content was successful otherwise False
|
|
"""
|
|
|
|
if not os.path.isfile(filename):
|
|
log.error(f"Inventory file {filename} seems to be not a regular file")
|
|
return False
|
|
|
|
with open(filename) as json_file:
|
|
try:
|
|
file_content = json.load(json_file)
|
|
except json.decoder.JSONDecodeError as e:
|
|
log.error(f"Inventory file {filename} contains invalid json: {e}")
|
|
return False
|
|
|
|
log.debug(f"Parsing inventory file {filename}")
|
|
|
|
# get inventory_layout_version
|
|
inventory_layout_version = grab(file_content, "meta.inventory_layout_version", fallback=0)
|
|
|
|
if version.parse(inventory_layout_version) < version.parse(self.minimum_check_redfish_version):
|
|
log.error(f"Inventory layout version '{inventory_layout_version}' of file {filename} not supported. "
|
|
f"Minimum layout version {self.minimum_check_redfish_version} required.")
|
|
|
|
return False
|
|
|
|
self.inventory_file_content = file_content
|
|
|
|
return True
|
|
|
|
def update_device(self):
|
|
|
|
system = grab(self.inventory_file_content, "inventory.system.0")
|
|
|
|
if system is None:
|
|
log.error(f"No system data found for '{self.device_object.get_display_name()}' in inventory file.")
|
|
return
|
|
|
|
# get status
|
|
status = "offline"
|
|
if get_string_or_none(grab(system, "power_state")) == "On":
|
|
status = "active"
|
|
|
|
serial = get_string_or_none(grab(system, "serial"))
|
|
name = get_string_or_none(grab(system, "host_name"))
|
|
|
|
device_data = {
|
|
"device_type": {
|
|
"model": get_string_or_none(grab(system, "model")),
|
|
"manufacturer": {
|
|
"name": get_string_or_none(grab(system, "manufacturer"))
|
|
},
|
|
},
|
|
"status": status,
|
|
"custom_fields": {
|
|
"health": get_string_or_none(grab(system, "health_status"))
|
|
}
|
|
}
|
|
|
|
if serial is not None:
|
|
device_data["serial"] = serial
|
|
if name is not None and self.overwrite_host_name is True:
|
|
device_data["name"] = name
|
|
|
|
self.device_object.update(data=device_data, source=self)
|
|
|
|
def update_power_supply(self):
|
|
|
|
# get power supplies
|
|
current_ps = list()
|
|
for ps in self.inventory.get_all_items(NBPowerPort):
|
|
if grab(ps, "data.device") == self.device_object:
|
|
current_ps.append(ps)
|
|
|
|
current_ps.sort(key=lambda x: grab(x, "data.name"))
|
|
|
|
ps_index = 0
|
|
ps_items = list()
|
|
for ps in grab(self.inventory_file_content, "inventory.power_supply", fallback=list()):
|
|
|
|
if grab(ps, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
ps_name = get_string_or_none(grab(ps, "name"))
|
|
input_voltage = get_string_or_none(grab(ps, "input_voltage"))
|
|
ps_type = get_string_or_none(grab(ps, "type"))
|
|
bay = get_string_or_none(grab(ps, "bay"))
|
|
capacity_in_watt = grab(ps, "capacity_in_watt")
|
|
firmware = get_string_or_none(grab(ps, "firmware"))
|
|
health_status = get_string_or_none(grab(ps, "health_status"))
|
|
model = get_string_or_none(grab(ps, "model"))
|
|
|
|
# set name
|
|
if ps_name.lower().startswith("hp"):
|
|
ps_name = "Power Supply"
|
|
|
|
if bay is not None and f"{bay}" not in ps_name:
|
|
ps_name += f" {bay}"
|
|
|
|
name_details = list()
|
|
if input_voltage is not None:
|
|
name_details.append(f"{input_voltage}V")
|
|
if ps_type is not None:
|
|
name_details.append(f"{ps_type}")
|
|
|
|
name = ps_name
|
|
if len(name_details) > 0:
|
|
name += f" ({' '.join(name_details)})"
|
|
|
|
# set description
|
|
description = list()
|
|
if model is not None:
|
|
description.append(f"Model: {model}")
|
|
|
|
size = None
|
|
if capacity_in_watt is not None:
|
|
size = f"{capacity_in_watt}W"
|
|
|
|
# compile inventory item data
|
|
ps_items.append({
|
|
"inventory_type": "Power Supply",
|
|
"health": health_status,
|
|
"description": description,
|
|
"full_name": name,
|
|
"serial": get_string_or_none(grab(ps, "serial")),
|
|
"manufacturer": get_string_or_none(grab(ps, "vendor")),
|
|
"part_number": get_string_or_none(grab(ps, "part_number")),
|
|
"firmware": firmware,
|
|
"size": size
|
|
})
|
|
|
|
# compile power supply data
|
|
ps_data = {
|
|
"name": name,
|
|
"device": self.device_object,
|
|
"description": ", ".join(description)
|
|
}
|
|
|
|
if capacity_in_watt is not None:
|
|
ps_data["maximum_draw"] = capacity_in_watt
|
|
if firmware is not None:
|
|
ps_data["custom_fields"] = {"firmware": firmware, "health": health_status}
|
|
|
|
# add/update power supply data
|
|
ps_object = grab(current_ps, f"{ps_index}")
|
|
if ps_object is None:
|
|
self.inventory.add_object(NBPowerPort, data=ps_data, source=self)
|
|
else:
|
|
if self.overwrite_power_supply_name is False:
|
|
del(ps_data["name"])
|
|
|
|
data_to_update = self.patch_data(ps_object, ps_data, self.overwrite_power_supply_attributes)
|
|
ps_object.update(data=data_to_update, source=self)
|
|
|
|
ps_index += 1
|
|
|
|
self.update_all_items(ps_items)
|
|
|
|
def update_fan(self):
|
|
|
|
items = list()
|
|
for fan in grab(self.inventory_file_content, "inventory.fan", fallback=list()):
|
|
|
|
if grab(fan, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
fan_name = get_string_or_none(grab(fan, "name"))
|
|
health_status = get_string_or_none(grab(fan, "health_status"))
|
|
physical_context = get_string_or_none(grab(fan, "physical_context"))
|
|
fan_id = get_string_or_none(grab(fan, "id"))
|
|
reading = get_string_or_none(grab(fan, "reading"))
|
|
reading_unit = get_string_or_none(grab(fan, "reading_unit"))
|
|
|
|
description = list()
|
|
speed = None
|
|
if physical_context is not None:
|
|
description.append(f"Context: {physical_context}")
|
|
|
|
if reading is not None and reading_unit is not None:
|
|
reading_unit = "%" if reading_unit.lower() == "percent" else reading_unit
|
|
speed = f"{reading}{reading_unit}"
|
|
|
|
items.append({
|
|
"inventory_type": "Fan",
|
|
"description": description,
|
|
"full_name": f"{fan_name} (ID: {fan_id})",
|
|
"health": health_status,
|
|
"speed": speed
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_memory(self):
|
|
|
|
items = list()
|
|
for memory in grab(self.inventory_file_content, "inventory.memory", fallback=list()):
|
|
|
|
if grab(memory, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
name = get_string_or_none(grab(memory, "name"))
|
|
health_status = get_string_or_none(grab(memory, "health_status"))
|
|
size_in_mb = grab(memory, "size_in_mb", fallback=0)
|
|
channel = get_string_or_none(grab(memory, "channel"))
|
|
slot = get_string_or_none(grab(memory, "slot"))
|
|
socket = get_string_or_none(grab(memory, "socket"))
|
|
speed = get_string_or_none(grab(memory, "speed"))
|
|
dimm_type = get_string_or_none(grab(memory, "type"))
|
|
|
|
if size_in_mb == 0 or (health_status is None and grab(memory, "operation_status") != "GoodInUse"):
|
|
continue
|
|
|
|
name_details = list()
|
|
if dimm_type is not None:
|
|
name_details.append(f"{dimm_type}")
|
|
|
|
if len(name_details) > 0:
|
|
name += f" ({' '.join(name_details)})"
|
|
|
|
description = list()
|
|
if socket is not None:
|
|
description.append(f"Socket: {socket}")
|
|
if channel is not None:
|
|
description.append(f"Channel: {channel}")
|
|
if slot is not None:
|
|
description.append(f"Slot: {slot}")
|
|
|
|
if speed is not None:
|
|
speed = f"{speed}MHz"
|
|
|
|
items.append({
|
|
"inventory_type": "DIMM",
|
|
"description": description,
|
|
"full_name": name,
|
|
"serial": get_string_or_none(grab(memory, "serial")),
|
|
"manufacturer": get_string_or_none(grab(memory, "manufacturer")),
|
|
"part_number": get_string_or_none(grab(memory, "part_number")),
|
|
"health": health_status,
|
|
"size": f"{size_in_mb / 1024}GB",
|
|
"speed": speed,
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_proc(self):
|
|
|
|
items = list()
|
|
for processor in grab(self.inventory_file_content, "inventory.processor", fallback=list()):
|
|
|
|
if grab(processor, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
instruction_set = get_string_or_none(grab(processor, "instruction_set"))
|
|
current_speed = grab(processor, "current_speed")
|
|
model = get_string_or_none(grab(processor, "model"))
|
|
cores = get_string_or_none(grab(processor, "cores"))
|
|
threads = get_string_or_none(grab(processor, "threads"))
|
|
socket = get_string_or_none(grab(processor, "socket"))
|
|
health_status = get_string_or_none(grab(processor, "health_status"))
|
|
|
|
name = f"{socket} ({model})"
|
|
|
|
if current_speed is not None:
|
|
current_speed = f"{current_speed / 1000}GHz"
|
|
size = None
|
|
if cores is not None and threads is not None:
|
|
size = f"{cores}/{threads}"
|
|
|
|
description = list()
|
|
if instruction_set is not None:
|
|
description.append(f"{instruction_set}")
|
|
if cores is not None:
|
|
description.append(f"Cores: {cores}")
|
|
if threads is not None:
|
|
description.append(f"Threads: {threads}")
|
|
|
|
items.append({
|
|
"inventory_type": "CPU",
|
|
"description": description,
|
|
"manufacturer": get_string_or_none(grab(processor, "manufacturer")),
|
|
"full_name": name,
|
|
"serial": get_string_or_none(grab(processor, "serial")),
|
|
"health": health_status,
|
|
"size": size,
|
|
"speed": current_speed
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_physical_drive(self):
|
|
|
|
items = list()
|
|
for pd in grab(self.inventory_file_content, "inventory.physical_drive", fallback=list()):
|
|
|
|
if grab(pd, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
pd_name = get_string_or_none(grab(pd, "name"))
|
|
firmware = get_string_or_none(grab(pd, "firmware"))
|
|
interface_type = get_string_or_none(grab(pd, "interface_type"))
|
|
health_status = get_string_or_none(grab(pd, "health_status"))
|
|
size_in_byte = grab(pd, "size_in_byte", fallback=0)
|
|
model = get_string_or_none(grab(pd, "model"))
|
|
speed_in_rpm = grab(pd, "speed_in_rpm")
|
|
location = get_string_or_none(grab(pd, "location"))
|
|
bay = get_string_or_none(grab(pd, "bay"))
|
|
pd_type = get_string_or_none(grab(pd, "type"))
|
|
serial = get_string_or_none(grab(pd, "serial"))
|
|
pd_id = get_string_or_none(grab(pd, "id"))
|
|
|
|
if serial is not None and serial in [x.get("serial") for x in items]:
|
|
continue
|
|
|
|
if pd_name.lower().startswith("hp"):
|
|
pd_name = "Physical Drive"
|
|
|
|
if location is not None and location not in pd_name:
|
|
pd_name += f" {location}"
|
|
elif bay is not None and bay not in pd_name:
|
|
pd_name += f" {bay}"
|
|
else:
|
|
pd_name += f" {pd_id}"
|
|
|
|
name = pd_name
|
|
|
|
name_details = list()
|
|
if pd_type is not None:
|
|
name_details.append(pd_type)
|
|
if model is not None and model not in name:
|
|
name_details.append(model)
|
|
|
|
name += f" ({' '.join(name_details)})"
|
|
|
|
description = list()
|
|
if interface_type is not None:
|
|
description.append(f"Interface: {interface_type}")
|
|
|
|
size = None
|
|
speed = None
|
|
if size_in_byte is not None and size_in_byte != 0:
|
|
size = "%dGB" % (size_in_byte / 1000 ** 3)
|
|
if speed_in_rpm is not None and speed_in_rpm != 0:
|
|
speed = f"{speed_in_rpm}RPM"
|
|
|
|
items.append({
|
|
"inventory_type": "Physical Drive",
|
|
"description": description,
|
|
"manufacturer": get_string_or_none(grab(pd, "manufacturer")),
|
|
"full_name": name,
|
|
"serial": serial,
|
|
"part_number": get_string_or_none(grab(pd, "part_number")),
|
|
"firmware": firmware,
|
|
"health": health_status,
|
|
"size": size,
|
|
"speed": speed
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_storage_controller(self):
|
|
|
|
items = list()
|
|
for sc in grab(self.inventory_file_content, "inventory.storage_controller", fallback=list()):
|
|
|
|
if grab(sc, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
name = get_string_or_none(grab(sc, "name"))
|
|
model = get_string_or_none(grab(sc, "model"))
|
|
location = get_string_or_none(grab(sc, "location"))
|
|
logical_drive_ids = grab(sc, "logical_drive_ids", fallback=list())
|
|
physical_drive_ids = grab(sc, "physical_drive_ids", fallback=list())
|
|
cache_size_in_mb = grab(sc, "cache_size_in_mb")
|
|
|
|
if name.lower().startswith("hp"):
|
|
name = model
|
|
|
|
if location is not None and location not in name:
|
|
name += f" {location}"
|
|
|
|
description = list()
|
|
if len(logical_drive_ids) > 0:
|
|
description.append(f"LDs: {len(logical_drive_ids)}")
|
|
if len(physical_drive_ids) > 0:
|
|
description.append(f"PDs: {len(physical_drive_ids)}")
|
|
|
|
size = None
|
|
if cache_size_in_mb is not None and cache_size_in_mb != 0:
|
|
size = f"{cache_size_in_mb}MB"
|
|
|
|
items.append({
|
|
"inventory_type": "Storage Controller",
|
|
"description": description,
|
|
"manufacturer": get_string_or_none(grab(sc, "manufacturer")),
|
|
"full_name": name,
|
|
"serial": get_string_or_none(grab(sc, "serial")),
|
|
"firmware": get_string_or_none(grab(sc, "firmware")),
|
|
"health": get_string_or_none(grab(sc, "health_status")),
|
|
"size": size
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_storage_enclosure(self):
|
|
|
|
items = list()
|
|
for se in grab(self.inventory_file_content, "inventory.storage_enclosure", fallback=list()):
|
|
|
|
if grab(se, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
name = get_string_or_none(grab(se, "name"))
|
|
model = get_string_or_none(grab(se, "model"))
|
|
location = get_string_or_none(grab(se, "location"))
|
|
num_bays = get_string_or_none(grab(se, "num_bays"))
|
|
|
|
if name.lower().startswith("hp") and model is not None:
|
|
name = model
|
|
|
|
if location is not None and location not in name:
|
|
name += f" {location}"
|
|
|
|
size = None
|
|
if num_bays is not None:
|
|
size = f"Bays: {num_bays}"
|
|
|
|
items.append({
|
|
"inventory_type": "Storage Enclosure",
|
|
"manufacturer": get_string_or_none(grab(se, "manufacturer")),
|
|
"full_name": name,
|
|
"serial": get_string_or_none(grab(se, "serial")),
|
|
"firmware": get_string_or_none(grab(se, "firmware")),
|
|
"health": get_string_or_none(grab(se, "health_status")),
|
|
"size": size
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_network_adapter(self):
|
|
|
|
items = list()
|
|
for adapter in grab(self.inventory_file_content, "inventory.network_adapter", fallback=list()):
|
|
|
|
if grab(adapter, "operation_status") in ["NotPresent", "Absent"]:
|
|
continue
|
|
|
|
adapter_name = get_string_or_none(grab(adapter, "name"))
|
|
adapter_id = get_string_or_none(grab(adapter, "id"))
|
|
model = get_string_or_none(grab(adapter, "model"))
|
|
firmware = get_string_or_none(grab(adapter, "firmware"))
|
|
health_status = get_string_or_none(grab(adapter, "health_status"))
|
|
serial = get_string_or_none(grab(adapter, "serial"))
|
|
num_ports = get_string_or_none(grab(adapter, "num_ports"))
|
|
manufacturer = get_string_or_none(grab(adapter, "manufacturer"))
|
|
|
|
if adapter_name.startswith("Network Adapter View"):
|
|
adapter_name = adapter_name.replace("Network Adapter View", "")
|
|
if adapter_name.startswith("Network Adapter"):
|
|
adapter_name = adapter_name.replace("Network Adapter", "")
|
|
adapter_name = adapter_name.strip()
|
|
|
|
if adapter_id != adapter_name:
|
|
if len(adapter_name) == 0:
|
|
adapter_name = adapter_id
|
|
else:
|
|
adapter_name = f"{adapter_name} ({adapter_id})"
|
|
|
|
if manufacturer is None:
|
|
if adapter_name.startswith("HPE"):
|
|
manufacturer = "HPE"
|
|
elif adapter_name.startswith("HP"):
|
|
manufacturer = "HP"
|
|
|
|
name = adapter_name
|
|
size = None
|
|
if model is not None:
|
|
name += f" - {model}"
|
|
if num_ports is not None:
|
|
size = f"{num_ports} Ports"
|
|
|
|
nic_type = NetBoxInterfaceType(name)
|
|
|
|
if adapter_id is not None:
|
|
self.interface_adapter_type_dict[adapter_id] = nic_type
|
|
|
|
items.append({
|
|
"inventory_type": "NIC",
|
|
"manufacturer": manufacturer,
|
|
"full_name": name,
|
|
"serial": serial,
|
|
"part_number": get_string_or_none(grab(adapter, "part_number")),
|
|
"firmware": firmware,
|
|
"health": health_status,
|
|
"size": size,
|
|
"speed": nic_type.get_speed_human()
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_network_interface(self):
|
|
|
|
port_data_dict = dict()
|
|
nic_ips = dict()
|
|
discovered_mac_list = list()
|
|
|
|
for nic_port in grab(self.inventory_file_content, "inventory.network_port", fallback=list()):
|
|
|
|
if grab(nic_port, "operation_status") in ["Disabled"]:
|
|
continue
|
|
|
|
port_name = get_string_or_none(grab(nic_port, "name"))
|
|
port_id = get_string_or_none(grab(nic_port, "id"))
|
|
mac_address = get_string_or_none(grab(nic_port, "addresses.0")) # get 1st mac address
|
|
link_status = get_string_or_none(grab(nic_port, "link_status"))
|
|
manager_ids = grab(nic_port, "manager_ids", fallback=list())
|
|
hostname = get_string_or_none(grab(nic_port, "hostname"))
|
|
health_status = get_string_or_none(grab(nic_port, "health_status"))
|
|
adapter_id = get_string_or_none(grab(nic_port, "adapter_id"))
|
|
|
|
mac_address = normalize_mac_address(mac_address)
|
|
|
|
if mac_address in discovered_mac_list:
|
|
continue
|
|
|
|
if mac_address is not None:
|
|
discovered_mac_list.append(mac_address)
|
|
|
|
if port_name is not None:
|
|
port_name += f" ({port_id})"
|
|
else:
|
|
port_name = port_id
|
|
|
|
# get port speed
|
|
link_speed = grab(nic_port, "capable_speed") or grab(nic_port, "current_speed") or 0
|
|
|
|
if link_speed == 0 and adapter_id is not None:
|
|
link_type = self.interface_adapter_type_dict.get(adapter_id)
|
|
else:
|
|
link_type = NetBoxInterfaceType(link_speed)
|
|
|
|
description = list()
|
|
if hostname is not None:
|
|
description.append(f"Hostname: {hostname}")
|
|
|
|
mgmt_only = False
|
|
# if number of managers belonging to this port is not 0 then it's a BMC port
|
|
if len(manager_ids) > 0:
|
|
mgmt_only = True
|
|
|
|
# get enabled state
|
|
enabled = False
|
|
# assume that a mgmt_only interface is always enabled as we retrieved data via redfish
|
|
if "up" in f"{link_status}".lower() or mgmt_only is True:
|
|
enabled = True
|
|
|
|
port_data_dict[port_name] = {
|
|
"inventory_type": "NIC Port",
|
|
"name": port_name,
|
|
"mac_address": mac_address,
|
|
"enabled": enabled,
|
|
"description": ", ".join(description),
|
|
"type": link_type.get_this_netbox_type(),
|
|
"mgmt_only": mgmt_only,
|
|
"health": health_status
|
|
}
|
|
|
|
# collect ip addresses
|
|
nic_ips[port_name] = list()
|
|
nic_ips[port_name].extend(grab(nic_port, "ipv4_addresses", fallback=list()))
|
|
nic_ips[port_name].extend(grab(nic_port, "ipv6_addresses", fallback=list()))
|
|
|
|
data = self.map_object_interfaces_to_current_interfaces(self.device_object, port_data_dict)
|
|
|
|
for port_name, port_data in port_data_dict.items():
|
|
|
|
# get current object for this interface if it exists
|
|
nic_object = data.get(port_name)
|
|
|
|
# unset "illegal" attributes
|
|
for attribute in ["inventory_type", "health"]:
|
|
if attribute in port_data:
|
|
del(port_data[attribute])
|
|
|
|
# del empty mac address attribute
|
|
if port_data.get("mac_address") is None:
|
|
del (port_data["mac_address"])
|
|
|
|
# create or update interface with data
|
|
if nic_object is None:
|
|
nic_object = self.inventory.add_object(NBInterface, data=port_data, source=self)
|
|
else:
|
|
if self.overwrite_interface_name is False and port_data.get("name") is not None:
|
|
del(port_data["name"])
|
|
|
|
this_link_type = port_data.get("type")
|
|
mgmt_only = port_data.get("mgmt_only")
|
|
data_to_update = self.patch_data(nic_object, port_data, self.overwrite_interface_attributes)
|
|
|
|
# always overwrite nic type if discovered
|
|
if port_data.get("type") != "other":
|
|
data_to_update["type"] = this_link_type
|
|
|
|
data_to_update["mgmt_only"] = mgmt_only
|
|
|
|
# update nic object
|
|
nic_object.update(data=data_to_update, source=self)
|
|
|
|
# check for interface ips
|
|
for nic_ip in nic_ips.get(port_name, list()):
|
|
|
|
if ip_valid_to_add_to_netbox(nic_ip, self.permitted_subnets, port_name) is False:
|
|
continue
|
|
|
|
self.add_ip_address(nic_ip, nic_object, grab(self.device_object, "data.site.data.name"))
|
|
|
|
def update_manager(self):
|
|
|
|
items = list()
|
|
for manager in grab(self.inventory_file_content, "inventory.manager", fallback=list()):
|
|
|
|
name = get_string_or_none(grab(manager, "name"))
|
|
model = get_string_or_none(grab(manager, "model"))
|
|
licenses = grab(manager, "licenses", fallback=list())
|
|
|
|
if name == "Manager" and model is not None:
|
|
name = model
|
|
|
|
if model is not None and model not in name:
|
|
name += f" {model}"
|
|
|
|
description = None
|
|
if len(licenses) > 0:
|
|
description = f"Licenses: %s" % (", ".join(licenses))
|
|
|
|
items.append({
|
|
"inventory_type": "Manager",
|
|
"description": description,
|
|
"full_name": name,
|
|
"manufacturer": grab(self.device_object, "data.device_type.data.manufacturer.data.name"),
|
|
"firmware": get_string_or_none(grab(manager, "firmware")),
|
|
"health": get_string_or_none(grab(manager, "health_status"))
|
|
})
|
|
|
|
self.update_all_items(items)
|
|
|
|
def update_all_items(self, items):
|
|
"""
|
|
Updates all inventory items of a certain type. Both (current and supplied list of items) will
|
|
be sorted by name and matched 1:1.
|
|
|
|
Parameters
|
|
----------
|
|
items: list
|
|
list of items to update
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
"""
|
|
|
|
if not isinstance(items, list):
|
|
raise ValueError(f"Value for 'items' must be type 'list' got: {items}")
|
|
|
|
if len(items) == 0:
|
|
return
|
|
|
|
# get device
|
|
inventory_type = grab(items, "0.inventory_type")
|
|
|
|
if inventory_type is None:
|
|
log.error(f"Unable to find inventory type for inventory item {items[0]}")
|
|
return
|
|
|
|
# get current inventory items for this device and type
|
|
current_inventory_items = dict()
|
|
for item in self.inventory.get_all_items(NBInventoryItem):
|
|
if grab(item, "data.device") == self.device_object and \
|
|
grab(item, "data.custom_fields.inventory-type") == inventory_type:
|
|
|
|
current_inventory_items[grab(item, "data.name")] = item
|
|
|
|
# sort items by display name
|
|
current_inventory_items = dict(sorted(current_inventory_items.items()))
|
|
|
|
# dict
|
|
# key: NB inventory object
|
|
# value: parsed data matching the exact name
|
|
matched_inventory = dict()
|
|
unmatched_inventory_items = list()
|
|
|
|
# try to match names to existing inventory
|
|
for item in items:
|
|
|
|
current_item = current_inventory_items.get(item.get("full_name"))
|
|
if current_item is not None:
|
|
# log.debug2(f"Found 1:1 name match for inventory item '{item.get('full_name')}'")
|
|
matched_inventory[current_item] = item
|
|
else:
|
|
# log.debug2(f"No current NetBox inventory item found for '{item.get('full_name')}'")
|
|
unmatched_inventory_items.append(item)
|
|
|
|
# sort unmatched items by full_name
|
|
unmatched_inventory_items.sort(key=lambda x: x.get("full_name"))
|
|
|
|
# iterate over current NetBox inventory items
|
|
# if name did not match try to assign unmatched items in alphabetical order
|
|
for nb_inventory_item in current_inventory_items.values():
|
|
|
|
if nb_inventory_item not in matched_inventory.keys():
|
|
if len(unmatched_inventory_items) > 0:
|
|
matched_inventory[nb_inventory_item] = unmatched_inventory_items.pop(0)
|
|
|
|
# set item health to absent if item can't be found in redfish inventory anymore
|
|
elif grab(nb_inventory_item, "data.custom_fields.health") != "Absent":
|
|
nb_inventory_item.update(data={"custom_fields": {"health": "Absent"}}, source=self)
|
|
|
|
# update items with matching NetBox inventory item
|
|
for inventory_object, inventory_data in matched_inventory.items():
|
|
self.update_item(inventory_data, inventory_object)
|
|
|
|
# create new inventory item in NetBox
|
|
for unmatched_inventory_item in unmatched_inventory_items:
|
|
self.update_item(unmatched_inventory_item)
|
|
|
|
def update_item(self, item_data: dict, inventory_object: NBInventoryItem = None):
|
|
"""
|
|
Updates a single inventory item with the supplied data.
|
|
If no item is provided a new one will be created.
|
|
|
|
Parameters
|
|
----------
|
|
item_data: dict
|
|
dict with data for item to update
|
|
inventory_object: NBInventoryItem, None
|
|
the NetBox inventory item to update.
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
"""
|
|
|
|
full_name = item_data.get("full_name")
|
|
label = item_data.get("label")
|
|
manufacturer = item_data.get("manufacturer")
|
|
part_number = item_data.get("part_number")
|
|
serial = item_data.get("serial")
|
|
description = item_data.get("description")
|
|
|
|
# compile inventory item data
|
|
inventory_data = {
|
|
"device": self.device_object,
|
|
"discovered": True,
|
|
"custom_fields": {
|
|
"firmware": item_data.get("firmware"),
|
|
"health": item_data.get("health"),
|
|
"inventory-type": item_data.get("inventory_type"),
|
|
"inventory-size": item_data.get("size"),
|
|
"inventory-speed": item_data.get("speed")
|
|
}
|
|
}
|
|
|
|
if isinstance(description, list):
|
|
description = ", ".join(description)
|
|
|
|
if full_name is not None:
|
|
inventory_data["name"] = full_name
|
|
if description is not None and len(description) > 0:
|
|
inventory_data["description"] = description
|
|
if serial is not None:
|
|
inventory_data["serial"] = serial
|
|
if manufacturer is not None:
|
|
inventory_data["manufacturer"] = {"name": manufacturer}
|
|
if part_number is not None:
|
|
inventory_data["part_id"] = part_number
|
|
if label is not None:
|
|
inventory_data["label"] = label
|
|
|
|
if inventory_object is None:
|
|
self.inventory.add_object(NBInventoryItem, data=inventory_data, source=self)
|
|
else:
|
|
inventory_object.update(data=inventory_data, source=self)
|
|
|
|
return
|
|
|
|
def add_update_custom_field(self, data):
|
|
"""
|
|
Adds/updates a NBCustomField object with data.
|
|
Update will only update the 'content_types' attribute.
|
|
|
|
Parameters
|
|
----------
|
|
data: dict
|
|
dictionary with NBCustomField attributes
|
|
|
|
Returns
|
|
-------
|
|
custom_field: NBCustomField
|
|
new or updated NBCustomField
|
|
"""
|
|
|
|
custom_field = self.inventory.get_by_data(NBCustomField, data={"name": data.get("name")})
|
|
|
|
if custom_field is None:
|
|
custom_field = self.inventory.add_object(NBCustomField, data=data, source=self)
|
|
else:
|
|
custom_field.update(data={"content_types": data.get("content_types")}, source=self)
|
|
|
|
return custom_field
|
|
|
|
def add_necessary_base_objects(self):
|
|
"""
|
|
Adds/updates source tag and all custom fields necessary for this source.
|
|
"""
|
|
|
|
# add source identification tag
|
|
self.inventory.add_update_object(NBTag, data={
|
|
"name": self.source_tag,
|
|
"description": f"Marks objects synced from check_redfish inventory '{self.name}' to this NetBox Instance."
|
|
})
|
|
|
|
# add Firmware
|
|
self.add_update_custom_field({
|
|
"name": "firmware",
|
|
"label": "Firmware",
|
|
"content_types": [
|
|
"dcim.inventoryitem",
|
|
"dcim.powerport"
|
|
],
|
|
"type": "text",
|
|
"description": "Item Firmware"
|
|
})
|
|
|
|
# add inventory item type
|
|
self.add_update_custom_field({
|
|
"name": "inventory-type",
|
|
"label": "Type",
|
|
"content_types": ["dcim.inventoryitem"],
|
|
"type": "text",
|
|
"description": "Describes the type of inventory item"
|
|
})
|
|
|
|
# add inventory item size
|
|
self.add_update_custom_field({
|
|
"name": "inventory-size",
|
|
"label": "Size",
|
|
"content_types": ["dcim.inventoryitem"],
|
|
"type": "text",
|
|
"description": "Describes the size of the inventory item if applicable"
|
|
})
|
|
|
|
# add inventory item speed
|
|
self.add_update_custom_field({
|
|
"name": "inventory-speed",
|
|
"label": "Speed",
|
|
"content_types": ["dcim.inventoryitem"],
|
|
"type": "text",
|
|
"description": "Describes the size of the inventory item if applicable"
|
|
})
|
|
|
|
# add health status
|
|
self.add_update_custom_field({
|
|
"name": "health",
|
|
"label": "Health",
|
|
"content_types": [
|
|
"dcim.inventoryitem",
|
|
"dcim.powerport",
|
|
"dcim.device"
|
|
],
|
|
"type": "text",
|
|
"description": "Shows the currently discovered health status"
|
|
})
|
|
|
|
# EOF
|