diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py index f6e272f..37c9726 100644 --- a/netbox_topology_views/api/serializers.py +++ b/netbox_topology_views/api/serializers.py @@ -50,4 +50,4 @@ class Meta: class IndividualOptionsSerializer(NetBoxModelSerializer): class Meta: model = IndividualOptions - fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout") + fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout", "disable_physics",) diff --git a/netbox_topology_views/api/views.py b/netbox_topology_views/api/views.py index 6741dd8..345a05e 100644 --- a/netbox_topology_views/api/views.py +++ b/netbox_topology_views/api/views.py @@ -109,7 +109,7 @@ def list(self, request): if request.GET: - filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks,show_neighbors = get_query_settings(request) + filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors, disable_physics = get_query_settings(request) # Read options from saved filters as NetBox does not handle custom plugin filters if "filter_id" in request.GET and request.GET["filter_id"] != '': @@ -128,6 +128,7 @@ def list(self, request): if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations'] if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks'] if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] + if disable_physics == False and 'disable_physics' in saved_filter_params: disable_physics = saved_filter_params['disable_physics'] except SavedFilter.DoesNotExist: # filter_id not found pass except Exception as inst: @@ -153,6 +154,7 @@ def list(self, request): group_locations=group_locations, group_racks=group_racks, group_id=group_id, + disable_physics=disable_physics, ) xml_data = export_data_to_xml(topo_data).decode('utf-8') diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index fa3e6ca..2d1ed79 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -36,7 +36,7 @@ class DeviceFilterForm( FieldSet( 'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless', - 'group_sites', 'group_locations', 'group_racks', name=_("Options") + 'group_sites', 'group_locations', 'group_racks', 'disable_physics', name=_("Options") ), FieldSet('id', name=_("Device")), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")), @@ -240,7 +240,7 @@ class DeviceFilterForm( ) show_unconnected = forms.NullBooleanField( label=_('Show Unconnected'), - required=False, + required=False, initial=False, widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES @@ -310,6 +310,9 @@ class DeviceFilterForm( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + disable_physics = forms.BooleanField( + label =_('Disable Physics'), required=False, initial=False + ) class CoordinateGroupsForm(NetBoxModelForm): fieldsets = ( @@ -502,6 +505,7 @@ class IndividualOptionsForm(NetBoxModelForm): 'group_locations', 'group_racks', 'draw_default_layout', + 'disable_physics', ), ) @@ -629,6 +633,13 @@ class IndividualOptionsForm(NetBoxModelForm): help_text=_('Enable this option if you want to draw the topology on ' 'the initial load (when you go to the topology plugin page).') ) + disable_physics = forms.BooleanField( + label=('Disable Physics'), + required=False, + initial=False, + help_text=_('When enables, no forces will act on nodes in the topology and they will only move ' + 'when dragged by hand. Devices without coordinates will be placed at (0, 0) by default.') + ) class Meta: model = IndividualOptions @@ -636,5 +647,6 @@ class Meta: 'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', - 'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'draw_default_layout' + 'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'draw_default_layout', + 'disable_physics' ] diff --git a/netbox_topology_views/migrations/0009_disable_physics.py b/netbox_topology_views/migrations/0009_disable_physics.py new file mode 100644 index 0000000..c3c7d4d --- /dev/null +++ b/netbox_topology_views/migrations/0009_disable_physics.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-04-29 14:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_topology_views', '0007_individualoptions_group_locations_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='individualoptions', + name='disable_physics', + field=models.BooleanField(default=False), + ), + ] \ No newline at end of file diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py index 7c14a47..9d7ce01 100644 --- a/netbox_topology_views/models.py +++ b/netbox_topology_views/models.py @@ -398,6 +398,9 @@ class IndividualOptions(NetBoxModel): draw_default_layout = models.BooleanField( default=False ) + disable_physics = models.BooleanField( + default=False + ) _netbox_private = True diff --git a/netbox_topology_views/utils.py b/netbox_topology_views/utils.py index 2217012..49450d8 100644 --- a/netbox_topology_views/utils.py +++ b/netbox_topology_views/utils.py @@ -177,8 +177,13 @@ def get_query_settings(request): if "show_neighbors" in request.GET: if request.GET["show_neighbors"] == "True" : show_neighbors = True - - return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors + + disable_physics = False + if "disable_physics" in request.GET: + if request.GET["disable_physics"] == "on": + disable_physics = True + + return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors, disable_physics class LinePattern(): wireless = [2, 10, 2, 10] diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index a2b4978..b0c32a6 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -1,5 +1,6 @@ import json from functools import reduce +import random from typing import DefaultDict, Dict, Optional, Union import time from itertools import chain @@ -97,7 +98,7 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): def create_node( - device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, group_id="default" + device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, disable_physics: bool, group_id="default", nnodes=1, ): node = {} node_content = "" @@ -200,11 +201,12 @@ def create_node( if not group_id: print('Exception occured while handling default group.') return node - + group = get_object_or_404(CoordinateGroup, pk=group_id) - node["physics"] = True - # Coords must be set even if no coords have been stored. Otherwise nodes with coords + node["physics"] = not disable_physics + + # Coords must be set even if no coords have been stored. Otherwise nodes with coords # will not be placed correctly by vis-network. node["x"] = 0 node["y"] = 0 @@ -222,6 +224,21 @@ def create_node( node["x"] = int(cords[0]) node["y"] = int(cords[1]) node["physics"] = False + elif disable_physics: + # draw devices clustered by their rack neighbours + # for center coordinates of a rack, use the rack name as seed (if available) + random.seed(device.rack.name if hasattr(device, "rack") else device.name) + # set the upper size of the graph + plot_size = int(nnodes / 12) + 1 + base_coords_x = random.randint(-plot_size * nnodes, plot_size * nnodes) + base_coords_y = random.randint(-plot_size * nnodes, plot_size * nnodes) + # make seed unique for devices for spread + if hasattr(device, "rack"): + random.seed(device.name) + # spread devices around the rack center coordinates + node["x"] = base_coords_x + random.randint(-2 * nnodes, 2 * nnodes) + node["y"] = base_coords_y + random.randint(-2 * nnodes, 2 * nnodes) + dev_title = " %s
" % (node_content) node["title"] = dev_title @@ -238,6 +255,7 @@ def create_edge( edge_id: int, termination_a: Dict, termination_b: Dict, + disable_physics: bool, circuit: Optional[Dict] = None, cable: Optional[Cable] = None, wireless: Optional[Dict] = None, @@ -300,6 +318,9 @@ def create_edge( if hasattr(cable, 'color') and cable.color != "": edge["color"] = "#" + cable.color + # Invert, because value must be False if disabled + edge["physics"] = not disable_physics + return edge @@ -339,6 +360,7 @@ def get_topology_data( group_locations: bool, group_racks: bool, group_id, + disable_physics: bool, ): supported_termination_types = [] @@ -436,6 +458,7 @@ def get_topology_data( circuit=circuit_model, termination_a=termination_a, termination_b=termination_b, + disable_physics=disable_physics, ) ) @@ -462,7 +485,7 @@ def get_topology_data( ] = circuit_termination.circuit for d in nodes_circuits.values(): - nodes.append(create_node(d, save_coords, group_id)) + nodes.append(create_node(d, save_coords, disable_physics, group_id)) if show_power: power_panels_ids = PowerPanel.objects.filter( @@ -505,6 +528,7 @@ def get_topology_data( termination_a=termination_a, termination_b=termination_b, power=True, + disable_physics=disable_physics, ) ) @@ -512,10 +536,10 @@ def get_topology_data( cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b for d in nodes_powerfeed.values(): - nodes.append(create_node(d, save_coords, group_id)) + nodes.append(create_node(d, save_coords, disable_physics, group_id)) for d in nodes_powerpanel.values(): - nodes.append(create_node(d, save_coords, group_id)) + nodes.append(create_node(d, save_coords, disable_physics, group_id)) if show_logical_connections: interfaces = Interface.objects.filter( @@ -543,7 +567,15 @@ def get_topology_data( 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=interface)) + edges.append( + create_edge( + edge_id=edge_ids, + termination_a=termination_a, + termination_b=termination_b, + interface=interface, + disable_physics=disable_physics + ) + ) nodes_devices[interface.device.id] = interface.device nodes_devices[destination.device.id] = destination.device @@ -623,6 +655,7 @@ def get_topology_data( cable=link.cable, termination_a=termination_a, termination_b=termination_b, + disable_physics=disable_physics, ) ) @@ -662,6 +695,7 @@ def get_topology_data( termination_a=termination_a, termination_b=termination_b, wireless=wireless, + disable_physics=disable_physics ) ) @@ -679,7 +713,7 @@ def get_topology_data( results = {} for d in nodes_devices.values(): - nodes.append(create_node(d, save_coords, group_id)) + nodes.append(create_node(d, save_coords, disable_physics, group_id, nnodes=len(nodes_devices.keys()))) results["nodes"] = nodes results["edges"] = edges @@ -710,7 +744,7 @@ def get(self, request): if request.GET: - filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors = get_query_settings(request) + filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors, disable_physics = get_query_settings(request) # Read options from saved filters as NetBox does not handle custom plugin filters if "filter_id" in request.GET and request.GET["filter_id"] != '': @@ -729,6 +763,7 @@ def get(self, request): if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations'] if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks'] if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] + if disable_physics == False and 'disable_physics' in saved_filter_params: disable_physics = saved_filter_params['disable_physics'] except SavedFilter.DoesNotExist: # filter_id not found pass except Exception as inst: @@ -756,6 +791,7 @@ def get(self, request): group_locations=group_locations, group_racks=group_racks, group_id=group_id, + disable_physics=disable_physics, ) else: @@ -779,7 +815,8 @@ def get(self, request): if individualOptions.group_sites: q['group_sites'] = "True" if individualOptions.group_locations: q['group_locations'] = "True" if individualOptions.group_racks: q['group_racks'] = "True" - if individualOptions.draw_default_layout: + if individualOptions.disable_physics: q['disable_physics'] = "True" + if individualOptions.draw_default_layout: q['draw_init'] = "True" else: q['draw_init'] = "False" @@ -1137,6 +1174,7 @@ def get(self, request): 'group_locations': queryset.group_locations, 'group_racks': queryset.group_racks, 'draw_default_layout': queryset.draw_default_layout, + 'disable_physics': queryset.disable_physics, }, )