Description
First off, thank you for this incredibly useful plugin! We're currently trying to implement it and have encountered a couple of issues preventing us from using it in an environment with ArubaCX and ArubaOS switches.
Symptom:
Interfaces on our devices are not being placed correctly according to the defined grid-template-areas. They often appear stacked or in the top-left corner of their container section. Console and USB ports seem to be placed correctly.
Environment:
NetBox Version: v4.2.6
NetBox Device View Plugin Version: 0.1.9
Python Version: 3.12.3
Database: PostgreSQL 14.10 on x86_64-pc-linux-gnu, compiled by gcc
OS: Centos 8
Observed Behavior:
- When inspecting the HTML for a device where interfaces are misplaced (e.g., an Aruba 6100 device named Switch015), we see the device view split into two different div containers, apparently based on component type and virtual chassis status/device name:
Interfaces are within a <div class="deviceview area d1">
section.
Console/USB ports are within a <div class="deviceview area dSwitch015">
section (where Switch015 is the device name).
- However, the core issue appears to be the grid-area value applied to the individual interface elements. For an interface named 1/1/15, we observe the following tag style:
<a ... style="grid-area: 1-15; " ... >
Our grid-template-areas definition for this device type has therefore been defined to expect the corresponding area name to be 1-15. The mismatch between the generated grid-area value (1-15) and the actual interface name (1/1/15) is preventing correct placement.
The content of the "Grid Template Area" CSS code assigned to the device_type in our NetBox instance is as follows:
.deviceview.area {
display: grid;
grid-template-areas:
". . 1-15 1-16 . . 1-13 1-14 . . 1-1 1-3 1-5 1-7 1-9 1-11 . . . . . . . . . . . . . . . ."
". . . . . . . . . . 1-2 1-4 1-6 1-8 1-10 1-12 . . . . . . . . . . . . usb-ble usb-c . .";
}
Root Cause Identification:
I have traced this back to the process_interfaces function in utils.py. The regular expression and subsequent stylename construction logic appear to incorrectly parse numerical interface names following an X/Y/Z convention.
The relevant code snippet from utils.py:
def process_interfaces(interfaces, ports_chassis, dev):
if interfaces is not None:
for itf in interfaces:
if itf.type == "virtual" or itf.type == "lag":
continue
# Problematic Regex and Stylename Construction
regex = r"^(?P<type>([a-zA-Z\-_]*))(\/|(?P<dev>[0-9]+).|\s)?((?P<module>[0-9]+).|\s)?((?P<port>[0-9]+))$"
matches = re.search(regex, itf.name.lower())
if matches:
itf.stylename = (
(matches["type"] or "")
+ (matches["module"] or "")
+ "-"
+ matches["port"]
)
else:
itf.stylename = re.sub(r"[^.a-zA-Z\d]", "-", itf.name.lower())
# ... (rest of the function)
if itf.stylename.isdigit():
itf.stylename = f"p{itf.stylename}"
if dev not in ports_chassis:
ports_chassis[dev] = []
ports_chassis[dev].append(itf)
return ports_chassis
For an interface name like 1/1/15, this logic incorrectly produces a stylename of 1-15 instead of the expected 1-1-15. This mismatch breaks the CSS Grid placement. The fallback regex for names that don't match also replaces / with -, but the primary regex logic takes precedence if it matches. Also, if the calculated style name is, for example, '1-1-15' it will fail the "if itf.stylename.isdigit()" test despite starting with a digit.
According to CSS rules, identifiers (which include the names used for grid-area and in grid-template-areas) cannot start with a digit. They also cannot start with a hyphen, or two hyphens followed by a digit.
So, a stylename like 1-1-15 is an invalid CSS identifier because it starts with the digit 1.
This means the isdigit() check in the code is only catching a very specific case (when the entire name is just numbers, like "15") but fails to catch other invalid cases where the name starts with a digit followed by other characters (like "1-1-15") or starts with a hyphen.
A more robust solution to this issue with the CSS rules would replace that 'if' clause with something like:
# Check if the stylename exists and starts with a digit or a hyphen
if itf.stylename and (itf.stylename[0].isdigit() or itf.stylename[0] == '-'):
itf.stylename = f"p{itf.stylename}"
We also observed that interfaces on devices not configured within a Virtual Chassis get processed with dev=member.vc_position (leading to the d1 class in our case for VC position 1), while console ports on non-Virtual Chassis devices get processed with dev=obj.name (leading to the dMXBTN015 class). This grouping is separate from the stylename issue but explains the different container classes we end up with.
Proposed Solutions/Suggestions:
1. Problem: Trailing Semicolon and Space in the style Attribute for Individual Ports.
Description: The HTML generated for individual port links ( tags) often includes a trailing semicolon and space within the style attribute (e.g., style="grid-area: 1-1-1; ") even when it's the only style property. This trailing ; can prevent the browser from correctly applying the grid-area property, causing the port to not be placed in its intended grid cell. This was particularly apparent when the conditional background-color style was not applied.
File(s) Involved: The Django template responsible for rendering the device view, likely netbox_device_view/templates/netbox_device_view/deviceview.html.
Proposed Solution: Modify the template logic to ensure the semicolon is only included in the style attribute if there are multiple style properties being added (e.g., both grid-area and background-color). The fix would involve moving the semicolon inside the conditional block that adds the background-color.
{% endif %}
"
style="grid-area: {{ int.stylename }};
{% if cable_colors == "on" and int.cable.color != "" %}
background-color: #{{ int.cable.color }}
{% endif %}
"
data-bs-toggle="tooltip"
becomes
{% endif %}
style="grid-area: {{ int.stylename }}{% if cable_colors == 'on' and int.cable.color != '' %}; background-color: #{{ int.cable.color }}{% endif %}"
data-bs-toggle="tooltip"
2. Problem: Incorrect stylename Generation Logic for Interfaces with Specific Naming Conventions (e.g., X/Y/Z).
Description: The Python code that generates the stylename (which is then used for the grid-area CSS property) for interfaces uses a regular expression that incorrectly parses certain naming formats, specifically names like 1/1/15. Instead of producing a stylename like 1-1-15 (which matches the likely format used in grid-template-areas), it generates 1-15.
File(s) Involved: The utility function responsible for processing interfaces, netbox_device_view/utils.py (specifically the process_interfaces function).
Proposed Solution: Modify the stylename generation logic within the process_interfaces function to correctly parse common interface naming conventions (like X/Y/Z, EthX/Y, etc.) and reliably produce a stylename format that matches the expected names used in the grid-template-areas CSS (e.g., converting 1/1/15 to 1-1-15). A simpler string manipulation method (like splitting and joining with hyphens) might be more robust than the current regex.
-
- Convert the entire interface name to lowercase.
-
- Replace common separators found in interface names (such as slashes /, dots ., and spaces \s) with a consistent single separator, like a hyphen (-). This can be done using a simple regular expression substitution.
-
- Clean up the resulting string to remove any potential multiple consecutive hyphens or leading/trailing hyphens that might have been introduced.
if interfaces is not None:
for itf in interfaces:
if itf.type == "virtual" or itf.type == "lag":
continue
regex = r"^(?P<type>([a-zA-Z\-_]*))(\/|(?P<dev>[0-9]+).|\s)?((?P<module>[0-9]+).|\s)?((?P<port>[0-9]+))$"
matches = re.search(regex, itf.name.lower())
if matches:
itf.stylename = (
(matches["type"] or "")
+ (matches["module"] or "")
+ "-"
+ matches["port"]
)
else:
itf.stylename = re.sub(r"[^.a-zA-Z\d]", "-", itf.name.lower())
becomes
if interfaces is not None:
for itf in interfaces:
# ... inside the for loop for itf ...
if itf.type == "virtual" or itf.type == "lag":
continue
# Convert to lowercase and replace common separators with hyphens
stylename = re.sub(r'[/\.\s]+', '-', itf.name.lower())
# Clean up multiple hyphens and leading/trailing hyphens
stylename = re.sub(r'-+', '-', stylename).strip('-')
# If the name becomes empty after cleaning, use a fallback
if not stylename:
stylename = f"iface-{itf.pk}" # Use a unique fallback
3. Problem: Insufficient Validation of Generated stylename as a Valid CSS Identifier.
Description: The code checks if the generated stylename isdigit() and prepends a p if it is. However, CSS identifiers cannot start with a digit or a hyphen, not just be entirely digits. This check misses cases like 1-1-15 (starts with a digit but includes hyphens) or names starting with a hyphen, which are also invalid CSS identifiers. If an invalid stylename is generated and not caught, the corresponding grid-area style will be ignored by the browser.
File(s) Involved: The utility functions generating stylenames, primarily netbox_device_view/utils.py (process_interfaces and potentially process_ports).
Proposed Solution: Replace the narrow isdigit() check with a more robust validation that checks if the stylename is empty or if its first character is a digit or a hyphen. If it is, prepend a valid character (like p) to ensure it becomes a valid CSS identifier before being used in the HTML and CSS.
if itf.stylename.isdigit():
itf.stylename = f"p{itf.stylename}"
becomes
if stylename and (stylename[0].isdigit() or stylename[0] == '-'):
itf.stylename = f"p{stylename}"
else:
itf.stylename = stylename
4. Problem: Interfaces on non-virtual-chassis joined devices identified as if they were attached to the first member of a virtual_chassis instead of the same device_named container that the ports are attached to.
Description:
- If a device is not part of a Virtual Chassis (obj.virtual_chassis is None):
-
- Interfaces (obj.interfaces.all()) are processed with dev=1.
-
- Console Ports (ConsolePort.objects.filter(...)) are processed with dev=obj.name (the device's name).
-
- Front and Rear Ports get dev="Front" and dev="Rear".
- If a device is part of a Virtual Chassis (else block):
-
- The code loops through the Virtual Chassis members.
-
- Interfaces and Console Ports for each member are processed with dev=member.vc_position (the member's position in the chassis, often starting at 1).
Proposed Solution:
in def prepare()
try:
if obj.virtual_chassis is None:
dv[1] = DeviceView.objects.get(device_type=obj.device_type).grid_template_area
modules[1] = obj.modules.all()
ports_chassis = process_interfaces(obj.interfaces.all(), ports_chassis, 1)
ports_chassis = process_ports(obj.frontports.all(), ports_chassis, "Front")
ports_chassis = process_ports(obj.rearports.all(), ports_chassis, "Rear")
ports_chassis = process_ports( ConsolePort.objects.filter(device_id=obj.id), ports_chassis, obj.name )
becomes
try:
if obj.virtual_chassis is None:
dv[1] = DeviceView.objects.get(device_type=obj.device_type).grid_template_area
modules[1] = obj.modules.all()
ports_chassis = process_interfaces(obj.interfaces.all(), ports_chassis, obj.name)
ports_chassis = process_ports(obj.frontports.all(), ports_chassis, "Front")
ports_chassis = process_ports(obj.rearports.all(), ports_chassis, "Rear")
ports_chassis = process_ports( ConsolePort.objects.filter(device_id=obj.id), ports_chassis, obj.name )