Skip to content

Commit b5e6b4e

Browse files
authored
netbox: Add show command for device information display (#1795)
Implements new 'osism netbox show' command to retrieve and display NetBox device information with the following features: - Device search by name or custom fields (alternative_name, inventory_hostname, external_hostname) - Display device details (name, type, role, site, status) - Show network information (out-of-band IP, primary IPv4/IPv6) - Display custom field parameters with proper YAML formatting: * dnsmasq_parameters * netplan_parameters * sonic_parameters * frr_parameters - Optional field filter argument for focused output - Case-insensitive partial field name matching Implementation: - Added Show class to osism/commands/netbox.py - Registered command entry point in setup.cfg - Uses existing NetBox connection (utils.nb) - Follows established command patterns - YAML formatting with proper indentation for readability Usage: osism netbox show <hostname> [field_filter] Examples: osism netbox show server01 osism netbox show server01 ip osism netbox show server01 parameters AI-assisted: Claude Code Signed-off-by: Christian Berendt <[email protected]>
1 parent 9ee49cd commit b5e6b4e

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

osism/commands/netbox.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from cliff.command import Command
77
from loguru import logger
8+
from tabulate import tabulate
89
import yaml
910

1011
from osism.tasks import conductor, netbox, handle_task
@@ -270,3 +271,157 @@ def take_action(self, parsed_args):
270271
yaml.dump(nbcli_config, fp, default_flow_style=False)
271272

272273
subprocess.call(f"/usr/local/bin/nbcli {type_console} {arguments}", shell=True)
274+
275+
276+
class Show(Command):
277+
def get_parser(self, prog_name):
278+
parser = super(Show, self).get_parser(prog_name)
279+
parser.add_argument(
280+
"host",
281+
nargs=1,
282+
type=str,
283+
help="Hostname or device name to search in NetBox",
284+
)
285+
parser.add_argument(
286+
"field",
287+
nargs="?",
288+
type=str,
289+
default=None,
290+
help="Optional field name filter (case-insensitive, partial match)",
291+
)
292+
return parser
293+
294+
def take_action(self, parsed_args):
295+
host = parsed_args.host[0]
296+
field_filter = parsed_args.field
297+
298+
# Check if NetBox connection is available
299+
if not utils.nb:
300+
logger.error("NetBox integration not configured.")
301+
return
302+
303+
# Search for device by name first
304+
devices = list(utils.nb.dcim.devices.filter(name=host))
305+
306+
# If not found by name, search by custom fields
307+
if not devices:
308+
# Search by alternative_name custom field
309+
devices = list(utils.nb.dcim.devices.filter(cf_alternative_name=host))
310+
311+
if not devices:
312+
# Search by inventory_hostname custom field
313+
devices = list(utils.nb.dcim.devices.filter(cf_inventory_hostname=host))
314+
315+
if not devices:
316+
# Search by external_hostname custom field
317+
devices = list(utils.nb.dcim.devices.filter(cf_external_hostname=host))
318+
319+
if not devices:
320+
logger.error(f"Device '{host}' not found in NetBox.")
321+
return
322+
323+
# Get the first matching device
324+
device = devices[0]
325+
326+
# Prepare table data for display
327+
table = []
328+
329+
# Add basic device information
330+
table.append(["Name", device.name])
331+
332+
# Device type - defensively accessed
333+
device_type = getattr(device, "device_type", None)
334+
table.append(["Device Type", str(device_type) if device_type else "N/A"])
335+
336+
# NetBox v3.x renamed device_role to role
337+
device_role = getattr(device, "role", None)
338+
table.append(["Device Role", str(device_role) if device_role else "N/A"])
339+
340+
# Site and status
341+
site = getattr(device, "site", None)
342+
table.append(["Site", str(site) if site else "N/A"])
343+
344+
status = getattr(device, "status", None)
345+
table.append(["Status", str(status) if status else "N/A"])
346+
347+
# Add out-of-band IP
348+
oob_ip = getattr(device, "oob_ip", None)
349+
table.append(["Out-of-band IP", str(oob_ip.address) if oob_ip else "N/A"])
350+
351+
# Add primary IPs - defensively accessed
352+
primary_ip4 = getattr(device, "primary_ip4", None)
353+
table.append(
354+
["Primary IPv4", str(primary_ip4.address) if primary_ip4 else "N/A"]
355+
)
356+
357+
primary_ip6 = getattr(device, "primary_ip6", None)
358+
table.append(
359+
["Primary IPv6", str(primary_ip6.address) if primary_ip6 else "N/A"]
360+
)
361+
362+
# Add custom fields if they exist - defensively accessed
363+
custom_fields = getattr(device, "custom_fields", {})
364+
365+
# Display custom field parameters with YAML formatting
366+
if custom_fields:
367+
# Define YAML custom fields for consistent formatting
368+
yaml_fields = [
369+
"dnsmasq_parameters",
370+
"netplan_parameters",
371+
"sonic_parameters",
372+
"frr_parameters",
373+
]
374+
375+
for field_name in yaml_fields:
376+
field_value = custom_fields.get(field_name, None)
377+
if field_value:
378+
try:
379+
# Parse YAML string if needed, or use value directly if already parsed
380+
if isinstance(field_value, str):
381+
parsed_value = yaml.safe_load(field_value)
382+
else:
383+
parsed_value = field_value
384+
385+
# Format as YAML with proper indentation and structure
386+
formatted_value = yaml.dump(
387+
parsed_value,
388+
default_flow_style=False,
389+
indent=2,
390+
sort_keys=False,
391+
width=80,
392+
).strip()
393+
394+
table.append([field_name, formatted_value])
395+
except Exception:
396+
# Fallback to string representation if YAML parsing fails
397+
table.append([field_name, str(field_value)])
398+
399+
# alternative_name
400+
alternative_name = custom_fields.get("alternative_name", None)
401+
if alternative_name:
402+
table.append(["Alternative Name", str(alternative_name)])
403+
404+
# inventory_hostname
405+
inventory_hostname = custom_fields.get("inventory_hostname", None)
406+
if inventory_hostname:
407+
table.append(["Inventory Hostname", str(inventory_hostname)])
408+
409+
# external_hostname
410+
external_hostname = custom_fields.get("external_hostname", None)
411+
if external_hostname:
412+
table.append(["External Hostname", str(external_hostname)])
413+
414+
# Apply field filter if specified
415+
if field_filter:
416+
filter_term = field_filter.lower()
417+
filtered_table = [row for row in table if filter_term in row[0].lower()]
418+
419+
if not filtered_table:
420+
logger.warning(f"No fields matching '{field_filter}' found")
421+
return
422+
423+
table = filtered_table
424+
425+
# Print formatted table
426+
result = tabulate(table, headers=["Field", "Value"], tablefmt="grid")
427+
print(result)

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ osism.commands:
9797
manage baremetal maintenance set = osism.commands.baremetal:BaremetalMaintenanceSet
9898
manage baremetal maintenance unset = osism.commands.baremetal:BaremetalMaintenanceUnset
9999
netbox = osism.commands.netbox:Console
100+
netbox show = osism.commands.netbox:Show
100101
get versions netbox = osism.commands.netbox:Versions
101102
noset bootstrap = osism.commands.noset:NoBootstrap
102103
noset maintenance = osism.commands.noset:NoMaintenance

0 commit comments

Comments
 (0)