diff --git a/README.md b/README.md index 4c1ecf0..a99528b 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ PLUGINS_CONFIG = { | ignore_cable_type | [] | The cable types that you want to ignore in the views | | preselected_tags | [] | The name of tags you want to preload | | draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) | +| hide_single_cable_logical_conns | False | (bool) Set to True if you want to hide duplicate cables & logical connections | + + ### Custom Images @@ -97,6 +100,31 @@ To change image with associated device use the `Images` page - it allows to map Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies +Select your options for the topology view: + +![preview image](doc/img/selection_options.png?raw=true "preview") + +
+
Hide Unconnected
+
Hide devices which have no connections.
+
Save Coordinates
+
Save the coordinates of devices in the topology view.
+
Please read the "Configure" chapter to set the allow_coordinates_saving option to True.
+
Show Cables
+
Show cable connections between devices, including cables connected to interfaces, front / rear ports, etc., in the topology view
+
Show Logical Connections
+
Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between + interfaces includes multiple cables (e.g., via patch panels), only the end interface connections are shown, not the + intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions.
+
Selecting both 'Show Cables' and 'Show Logical Interfaces' will sometimes result in a topology view with 2 cables per connection as these devices are diretly connected. Select only one of the options or set `hide_single_cable_logical_conns` to True.
+
Show Circuit Terminations
+
Show connections which end at a circuit termination in the topology view.
+
Show Wireless Links
+
Show wireless connections in the topology view.
+
Show Power Feeds
+
Show power connections from power feeds in the topology view.
+
+ ### Update Run `pip install netbox-topology-views --upgrade` in your venv. diff --git a/doc/img/selection_options.png b/doc/img/selection_options.png new file mode 100644 index 0000000..dac7dd6 Binary files /dev/null and b/doc/img/selection_options.png differ diff --git a/netbox_topology_views/__init__.py b/netbox_topology_views/__init__.py index 250e154..5c79d0a 100644 --- a/netbox_topology_views/__init__.py +++ b/netbox_topology_views/__init__.py @@ -29,6 +29,7 @@ class TopologyViewsConfig(PluginConfig): "always_save_coordinates": False, "preselected_tags": [], "draw_default_layout": False, + "hide_single_cable_logical_conns": False, } diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index e517ca9..1b971cc 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -34,8 +34,11 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): "filter_id", "hide_unconnected", "save_coords", + "show_cables", "show_circuit", + "show_logical_connections", "show_power", + "show_wireless", ), ), ( @@ -104,6 +107,15 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): hide_unconnected = forms.BooleanField( label=_("Hide Unconnected"), required=False, initial=False ) + show_logical_connections = forms.BooleanField( + label =_("Show Logical Connections"), required=False, initial=False + ) + show_cables = forms.BooleanField( + label =_("Show Cables"), required=False, initial=False + ) + show_wireless = forms.BooleanField( + label =_("Show Wireless Links"), required=False, initial=False + ) show_circuit = forms.BooleanField( label=_("Show Circuit Terminations"), required=False, initial=False ) diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 7568e50..ae7f823 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -9,6 +9,7 @@ from dcim.models import ( Cable, CableTermination, Device, + device_components, DeviceRole, FrontPort, Interface, @@ -177,6 +178,7 @@ def create_edge( cable: Optional[Cable] = None, wireless: Optional[Dict] = None, power: Optional[bool] = None, + interface: Optional[bool] = None, ): cable_a_name = ( "device A name unknown" @@ -217,6 +219,13 @@ def create_edge( edge["dashes"] = [5, 5, 3, 3] title = "Power Connection" + elif interface is not None: + title = "Interface Connection" + edge["width"] = 3 + edge["dashes"] = [1, 10, 1, 10] + edge["arrows"] = {"to": {"enabled": True, "scaleFactor": 0.5}, "from": {"enabled": True, "scaleFactor": 0.5}} + edge["color"] = '#f1c232' + edge[ "title" ] = f"{title} between
{cable_a_dev_name} [{cable_a_name}]
{cable_b_dev_name} [{cable_b_name}]" @@ -251,8 +260,11 @@ def get_topology_data( queryset: QuerySet, hide_unconnected: bool, save_coords: bool, + show_cables: bool, show_circuit: bool, + show_logical_connections: bool, show_power: bool, + show_wireless: bool, ): if not queryset: return None @@ -266,9 +278,15 @@ def get_topology_data( nodes_powerfeed: Dict[int, PowerFeed] = {} nodes_provider_networks = {} cable_ids = DefaultDict(dict) + interface_ids = DefaultDict(dict) ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][ "ignore_cable_type" ] + hide_single_cable_logical_conns = bool( + settings.PLUGINS_CONFIG["netbox_topology_views"][ + "hide_single_cable_logical_conns" + ] + ) device_ids = [d.pk for d in queryset] site_ids = [d.site_id for d in queryset] @@ -345,15 +363,6 @@ def get_topology_data( for d in nodes_circuits.values(): nodes.append(create_node(d, save_coords)) - links: QuerySet[CableTermination] = CableTermination.objects.filter( - Q(_device_id__in=device_ids) - ).select_related("termination_type") - - wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter( - Q(_interface_a_device_id__in=device_ids) - & Q(_interface_b_device_id__in=device_ids) - ) - if show_power: power_panels_ids = PowerPanel.objects.filter( Q(site_id__in=site_ids) @@ -407,112 +416,153 @@ def get_topology_data( for d in nodes_powerpanel.values(): nodes.append(create_node(d, save_coords)) - for link in links: - if link.termination_type.name in ignore_cable_type: - continue - - # Normal device cables - if link.termination_type.name in supported_termination_types: - complete_link = False - if link.cable_end == "A": - if link.cable_id not in cable_ids: - cable_ids[link.cable_id] = {} - else: - if "B" in cable_ids[link.cable_id]: - if cable_ids[link.cable_id]["B"] is not None: - complete_link = True - elif link.cable_end == "B": - if link.cable_id not in cable_ids: - cable_ids[link.cable_id] = {} - else: - if "A" in cable_ids[link.cable_id]: - if cable_ids[link.cable_id]["A"] is not None: - complete_link = True - else: - print("Unkown cable end") - cable_ids[link.cable_id][link.cable_end] = link - - if complete_link: - edge_ids += 1 - if isinstance(cable_ids[link.cable_id]["B"], CableTermination): - if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices: - nodes_devices[ - cable_ids[link.cable_id]["B"]._device_id - ] = cable_ids[link.cable_id]["B"].termination.device - termination_b = { - "termination_name": cable_ids[link.cable_id][ - "B" - ].termination.name, - "termination_device_name": cable_ids[link.cable_id][ - "B" - ].termination.device.name, - "device_id": cable_ids[link.cable_id][ - "B" - ].termination.device_id, - } - else: - termination_b = cable_ids[link.cable_id]["B"] - - if isinstance(cable_ids[link.cable_id]["A"], CableTermination): - if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices: - nodes_devices[ - cable_ids[link.cable_id]["A"]._device_id - ] = cable_ids[link.cable_id]["A"].termination.device - termination_a = { - "termination_name": cable_ids[link.cable_id][ - "A" - ].termination.name, - "termination_device_name": cable_ids[link.cable_id][ - "A" - ].termination.device.name, - "device_id": cable_ids[link.cable_id][ - "A" - ].termination.device_id, - } - else: - termination_a = cable_ids[link.cable_id]["A"] - - edges.append( - create_edge( - edge_id=edge_ids, - cable=link.cable, - termination_a=termination_a, - termination_b=termination_b, - ) - ) - - for wlan_link in wlan_links: - if wlan_link.interface_a.device_id not in nodes_devices: - nodes_devices[ - wlan_link.interface_a.device.pk - ] = wlan_link.interface_a.device - if wlan_link.interface_b.device_id not in nodes_devices: - nodes_devices[ - wlan_link.interface_b.device.pk - ] = wlan_link.interface_b.device - - termination_a = { - "termination_name": wlan_link.interface_a.name, - "termination_device_name": wlan_link.interface_a.device.name, - "device_id": wlan_link.interface_a.device_id, - } - termination_b = { - "termination_name": wlan_link.interface_b.name, - "termination_device_name": wlan_link.interface_b.device.name, - "device_id": wlan_link.interface_b.device_id, - } - wireless = {"ssid": wlan_link.ssid} - - edge_ids += 1 - edges.append( - create_edge( - edge_id=edge_ids, - termination_a=termination_a, - termination_b=termination_b, - wireless=wireless, - ) + if show_logical_connections: + interfaces = Interface.objects.filter( + Q(_path__is_complete=True) & Q(device_id__in=device_ids) ) + for interface in interfaces: + # print('{} {} {} {}'.format(interface.device.name, interface.name, interface._path.destinations[0].device.name, interface._path.destinations[0].name)) + for destination in interface._path.destinations: + if isinstance(destination, device_components.Interface): + if destination.device.id not in device_ids: + # print('Destination interface not in device queryset, ignoring') + continue + + if destination.id in interface_ids: + # we've already captured the destination interface, ignore this connection + # print('Destination interface already exists, ignoring') + continue + + if hide_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables: + # interface connection is the same as the cable connection, ignore this connection + continue + + interface_ids[interface.id]=interface + edge_ids += 1 + termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id } + termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id } + edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=True)) + nodes_devices[interface.device.id] = interface.device + nodes_devices[destination.device.id] = destination.device + + if show_cables: + links: QuerySet[CableTermination] = CableTermination.objects.filter( + Q(_device_id__in=device_ids) + ).select_related("termination_type") + + for link in links: + if link.termination_type.name in ignore_cable_type: + continue + + # Normal device cables + if link.termination_type.name in supported_termination_types: + complete_link = False + if link.cable_end == "A": + if link.cable_id not in cable_ids: + cable_ids[link.cable_id] = {} + else: + if "B" in cable_ids[link.cable_id]: + if cable_ids[link.cable_id]["B"] is not None: + complete_link = True + elif link.cable_end == "B": + if link.cable_id not in cable_ids: + cable_ids[link.cable_id] = {} + else: + if "A" in cable_ids[link.cable_id]: + if cable_ids[link.cable_id]["A"] is not None: + complete_link = True + else: + print("Unkown cable end") + cable_ids[link.cable_id][link.cable_end] = link + + if complete_link: + edge_ids += 1 + if isinstance(cable_ids[link.cable_id]["B"], CableTermination): + if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices: + nodes_devices[ + cable_ids[link.cable_id]["B"]._device_id + ] = cable_ids[link.cable_id]["B"].termination.device + termination_b = { + "termination_name": cable_ids[link.cable_id][ + "B" + ].termination.name, + "termination_device_name": cable_ids[link.cable_id][ + "B" + ].termination.device.name, + "device_id": cable_ids[link.cable_id][ + "B" + ].termination.device_id, + } + else: + termination_b = cable_ids[link.cable_id]["B"] + + if isinstance(cable_ids[link.cable_id]["A"], CableTermination): + if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices: + nodes_devices[ + cable_ids[link.cable_id]["A"]._device_id + ] = cable_ids[link.cable_id]["A"].termination.device + termination_a = { + "termination_name": cable_ids[link.cable_id][ + "A" + ].termination.name, + "termination_device_name": cable_ids[link.cable_id][ + "A" + ].termination.device.name, + "device_id": cable_ids[link.cable_id][ + "A" + ].termination.device_id, + } + else: + termination_a = cable_ids[link.cable_id]["A"] + + edges.append( + create_edge( + edge_id=edge_ids, + cable=link.cable, + termination_a=termination_a, + termination_b=termination_b, + ) + ) + + if show_wireless: + wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter( + Q(_interface_a_device_id__in=device_ids) + & Q(_interface_b_device_id__in=device_ids) + ) + + for wlan_link in wlan_links: + if wlan_link.interface_a.device_id not in nodes_devices: + nodes_devices[ + wlan_link.interface_a.device.pk + ] = wlan_link.interface_a.device + if wlan_link.interface_b.device_id not in nodes_devices: + nodes_devices[ + wlan_link.interface_b.device.pk + ] = wlan_link.interface_b.device + + termination_a = { + "termination_name": wlan_link.interface_a.name, + "termination_device_name": wlan_link.interface_a.device.name, + "device_id": wlan_link.interface_a.device_id, + } + termination_b = { + "termination_name": wlan_link.interface_b.name, + "termination_device_name": wlan_link.interface_b.device.name, + "device_id": wlan_link.interface_b.device_id, + } + wireless = {"ssid": wlan_link.ssid} + + edge_ids += 1 + edges.append( + create_edge( + edge_id=edge_ids, + termination_a=termination_a, + termination_b=termination_b, + wireless=wireless, + ) + ) + for qs_device in queryset: if qs_device.pk not in nodes_devices and not hide_unconnected: nodes_devices[qs_device.pk] = qs_device @@ -564,22 +614,43 @@ class TopologyHomeView(PermissionRequiredMixin, View): if request.GET["show_circuit"] == "on": show_circuit = True + show_logical_connections = False + if "show_logical_connections" in request.GET: + if request.GET["show_logical_connections"] == "on" : + show_logical_connections = True + + show_cables = False + if "show_cables" in request.GET: + if request.GET["show_cables"] == "on" : + show_cables = True + + show_wireless = False + if "show_wireless" in request.GET: + if request.GET["show_wireless"] == "on" : + show_wireless = True + if "draw_init" in request.GET: if request.GET["draw_init"].lower() == "true": topo_data = get_topology_data( self.queryset, hide_unconnected, save_coords, + show_cables, show_circuit, + show_logical_connections, show_power, + show_wireless, ) else: topo_data = get_topology_data( self.queryset, hide_unconnected, save_coords, + show_cables, show_circuit, + show_logical_connections, show_power, + show_wireless, ) else: preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][