From dfb0b532b94a75d80c859ed4a82931020907e703 Mon Sep 17 00:00:00 2001 From: tgrusendorf <73901582+tgrusendorf@users.noreply.github.com> Date: Tue, 27 Dec 2022 06:49:34 -0700 Subject: [PATCH] Closes #151: Implement interface end to end connection view (#188) * Added end to end interface connections, made showing cables and wireless optional * Fixed 'Hide Unconnected' and other bugs * rename var + extra check * Add filter selection image for readme * Added guidance on using the selection options * Renaming to selection_options * Renamed filter_selection to selection_options * Update README.md * Bug fix * fix parameter order * differentiate line type for interface connections * allow both cables and logical connections * Change logical connection line type Co-authored-by: Mario * Hide redundant connections, configurable options * updated readme + removed visjs setting from settings * Update README.md Co-authored-by: Mario * updated readme for 'Show Logical Connections' Co-authored-by: tagrusendorf Co-authored-by: mattieserver <3049868+mattieserver@users.noreply.github.com> Co-authored-by: Mario Co-authored-by: Mattijs Vanhaverbeke --- README.md | 28 +++ doc/img/selection_options.png | Bin 0 -> 10120 bytes netbox_topology_views/__init__.py | 1 + netbox_topology_views/forms.py | 12 ++ netbox_topology_views/views.py | 297 ++++++++++++++++++------------ 5 files changed, 225 insertions(+), 113 deletions(-) create mode 100644 doc/img/selection_options.png 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 0000000000000000000000000000000000000000..dac7dd64fe0d41c70173d2c40731c1f522461b7e GIT binary patch literal 10120 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 Ad;kCd literal 0 HcmV?d00001 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"][