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
37 changes: 23 additions & 14 deletions netbox_agent/arp_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def _scan_arp_scan(interface: str, timeout: int) -> list[tuple[str, str]]:
try:
result = subprocess.run(
["arp-scan", "--localnet", f"--interface={interface}", "--plain"],
capture_output=True, text=True, timeout=timeout,
capture_output=True,
text=True,
timeout=timeout,
)
for line in result.stdout.strip().splitlines():
# arp-scan --plain output: IP\tMAC\tVendor (or IP\tMAC)
Expand Down Expand Up @@ -115,7 +117,9 @@ def _scan_nmap(interface: str, timeout: int) -> list[tuple[str, str]]:
try:
result = subprocess.run(
["nmap", "-sn", "-oX", "-", "-e", interface, cidr],
capture_output=True, text=True, timeout=timeout,
capture_output=True,
text=True,
timeout=timeout,
)
root = ET.fromstring(result.stdout)
for host in root.findall("host"):
Expand Down Expand Up @@ -149,7 +153,9 @@ def _scan_ip_neigh() -> list[tuple[str, str]]:
try:
result = subprocess.run(
["ip", "-j", "neigh", "show"],
capture_output=True, text=True, timeout=10,
capture_output=True,
text=True,
timeout=10,
)
entries = json.loads(result.stdout) if result.stdout.strip() else []
for entry in entries:
Expand Down Expand Up @@ -195,17 +201,17 @@ def _get_scan_interfaces(config) -> list[str]:
# These waste time (arp-scan times out on bridges) and produce
# noise (container-internal MACs are not useful for inventory).
_SKIP_PREFIXES = (
"br-", # Docker custom network bridges
"veth", # Docker/container veth pairs
"vnet", # libvirt/KVM virtual NICs
"virbr", # libvirt default bridge
"cni", # Kubernetes CNI
"br-", # Docker custom network bridges
"veth", # Docker/container veth pairs
"vnet", # libvirt/KVM virtual NICs
"virbr", # libvirt default bridge
"cni", # Kubernetes CNI
"flannel", # Kubernetes flannel overlay
"calico", # Kubernetes Calico
"tunl", # tunnel interfaces
"podman", # Podman container bridge
"wg", # WireGuard tunnels
"tailscale", # Tailscale tunnel
"calico", # Kubernetes Calico
"tunl", # tunnel interfaces
"podman", # Podman container bridge
"wg", # WireGuard tunnels
"tailscale", # Tailscale tunnel
)

for iface in sorted(os.listdir("/sys/class/net/")):
Expand All @@ -230,6 +236,7 @@ def _get_scan_interfaces(config) -> list[str]:
# Check for IPv4 address (skip interfaces with no IP)
try:
import netifaces

addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
if not addrs:
continue
Expand Down Expand Up @@ -319,7 +326,9 @@ def scan_and_report(config) -> dict:
timeout=post_timeout,
)
result["pairs_submitted"] = len(pairs_list)
result["response"] = resp.json() if resp.ok else {"status_code": resp.status_code, "text": resp.text}
result["response"] = (
resp.json() if resp.ok else {"status_code": resp.status_code, "text": resp.text}
)
if not resp.ok:
logging.warning("ARP report POST failed: HTTP %d — %s", resp.status_code, resp.text)
else:
Expand Down
11 changes: 7 additions & 4 deletions netbox_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,22 @@ def run(config):
or network_only
):
server.netbox_create_or_update(
config, deps=deps, network_only=network_only, state=state,
config,
deps=deps,
network_only=network_only,
state=state,
)
if config.debug:
server.print_debug()

# ARP neighbor reporting (after main sync)
arp_enabled = (
getattr(config, "arp_report", None)
and getattr(config.arp_report, "enabled", False)
arp_enabled = getattr(config, "arp_report", None) and getattr(
config.arp_report, "enabled", False
)
arp_flag = getattr(config, "arp_report_flag", False)
if arp_enabled or arp_flag:
from netbox_agent.arp_reporter import scan_and_report

try:
arp_result = scan_and_report(config)
logging.info(
Expand Down
16 changes: 8 additions & 8 deletions netbox_agent/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

# tool_name -> (description, required)
TOOLS = {
"dmidecode": ("DMI/SMBIOS data (serial, chassis, PSUs, DIMMs)", True),
"lshw": ("Hardware tree (GPUs, NICs, storage fallback)", True),
"lsblk": ("Block device enumeration (primary storage)", False),
"lscpu": ("CPU socket detection (primary CPU path)", False),
"ipmitool": ("IPMI/BMC data (OOB IP, MAC, asset tag)", False),
"ethtool": ("NIC speed/duplex detection", False),
"lldpctl": ("LLDP neighbor discovery (auto-cabling)", False),
"nvme": ("NVMe device enrichment (vendor, firmware)", False),
"dmidecode": ("DMI/SMBIOS data (serial, chassis, PSUs, DIMMs)", True),
"lshw": ("Hardware tree (GPUs, NICs, storage fallback)", True),
"lsblk": ("Block device enumeration (primary storage)", False),
"lscpu": ("CPU socket detection (primary CPU path)", False),
"ipmitool": ("IPMI/BMC data (OOB IP, MAC, asset tag)", False),
"ethtool": ("NIC speed/duplex detection", False),
"lldpctl": ("LLDP neighbor discovery (auto-cabling)", False),
"nvme": ("NVMe device enrichment (vendor, firmware)", False),
"nvidia-smi": ("NVIDIA GPU serial numbers", False),
}

Expand Down
2 changes: 1 addition & 1 deletion netbox_agent/ethtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _parse_ethtool_module_output(self):
if colon <= 0:
continue
key = line[:colon].strip()
value = line[colon + 1:].strip()
value = line[colon + 1 :].strip()

if key in module_field_map:
field_key = module_field_map[key]
Expand Down
12 changes: 5 additions & 7 deletions netbox_agent/ipmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ def __init__(self):
return

for ch in _CHANNELS:
ret, output = subprocess.getstatusoutput(
f"ipmitool lan print {ch}"
)
ret, output = subprocess.getstatusoutput(f"ipmitool lan print {ch}")
if ret != 0:
continue

Expand All @@ -41,9 +39,7 @@ def __init__(self):
self.output = output
self.channel = ch
ip = self._extract_field(output, "IP Address") or "0.0.0.0"
logger.debug(
"IPMI: valid response on channel %d (MAC=%s, IP=%s)", ch, mac, ip
)
logger.debug("IPMI: valid response on channel %d (MAC=%s, IP=%s)", ch, mac, ip)
break
else:
logger.warning("IPMI: no valid response on channels %s", _CHANNELS)
Expand Down Expand Up @@ -81,7 +77,9 @@ def parse(self):
if ip and ip != "0.0.0.0":
ip_list = [f"{ip}/32"]
else:
logger.info("IPMI: MAC=%s but IP unassigned (0.0.0.0) — interface created without IP", mac)
logger.info(
"IPMI: MAC=%s but IP unassigned (0.0.0.0) — interface created without IP", mac
)

return {
"name": "IPMI",
Expand Down
39 changes: 25 additions & 14 deletions netbox_agent/lshw.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,22 @@ def find_gpus(self, obj):
# These are IOMMU, host bridges, system peripherals, etc. that lshw
# classifies as "generic" but are not compute accelerators.
_INFRA_DESCRIPTIONS = {
"iommu", "system peripheral", "generic system peripheral",
"non-essential instrumentation", "encryption controller",
"host bridge", "pci bridge", "isa bridge", "smi bridge",
"signal processing controller", "communication controller",
"pic", "dma controller", "timer",
"performance counters", # Intel CPU uncore PMU counters
"scsi enclosure", # Dell storage enclosure managers (Fryer U.2 etc.)
"iommu",
"system peripheral",
"generic system peripheral",
"non-essential instrumentation",
"encryption controller",
"host bridge",
"pci bridge",
"isa bridge",
"smi bridge",
"signal processing controller",
"communication controller",
"pic",
"dma controller",
"timer",
"performance counters", # Intel CPU uncore PMU counters
"scsi enclosure", # Dell storage enclosure managers (Fryer U.2 etc.)
}

def find_accelerators(self, obj):
Expand Down Expand Up @@ -227,13 +236,15 @@ def find_accelerators(self, obj):
return

# Everything else is a true accelerator (Pliops, FPGA, custom hardware)
self.accelerators.append({
"product": obj.get("product", "Unknown Accelerator"),
"vendor": vendor,
"description": obj.get("description", ""),
"businfo": obj.get("businfo", ""),
"class": obj.get("class", ""),
})
self.accelerators.append(
{
"product": obj.get("product", "Unknown Accelerator"),
"vendor": vendor,
"description": obj.get("description", ""),
"businfo": obj.get("businfo", ""),
"class": obj.get("class", ""),
}
)

# PCI device classes that indicate compute accelerators (not CPUs, not GPUs)
_ACCELERATOR_CLASSES = {"coprocessor", "generic", "processing"}
Expand Down
9 changes: 4 additions & 5 deletions netbox_agent/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,17 @@ def get_device_type(model, manufacturer=None):
device_type = nb.dcim.device_types.get(model=model)
if device_type is None and manufacturer:
mfr = get_or_create_manufacturer(manufacturer)
logging.info(
'Auto-creating DeviceType "%s" (manufacturer: %s)', model, manufacturer
)
logging.info('Auto-creating DeviceType "%s" (manufacturer: %s)', model, manufacturer)
device_type = nb.dcim.device_types.create(
model=model,
slug=slugify(model),
manufacturer=mfr.id,
)
if device_type is None:
raise Exception(
'DeviceType "{}" does not exist and no manufacturer provided'
" for auto-creation".format(model)
'DeviceType "{}" does not exist and no manufacturer provided for auto-creation'.format(
model
)
)
return device_type

Expand Down
Loading
Loading