diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py index 6f5034e..27219a5 100644 --- a/netbox_topology_views/api/serializers.py +++ b/netbox_topology_views/api/serializers.py @@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer): 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", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size") + 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", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size", "node_label_items") diff --git a/netbox_topology_views/api/views.py b/netbox_topology_views/api/views.py index 2957fcf..0600bdd 100644 --- a/netbox_topology_views/api/views.py +++ b/netbox_topology_views/api/views.py @@ -114,7 +114,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet): if request.GET: - filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = get_query_settings(request) + filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size, node_label_items = 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"] != '': @@ -138,6 +138,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet): if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] if straight_cables == False and 'straight_cables' in saved_filter_params: show_neighbors = saved_filter_params['straight_cables'] if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size'] + if node_label_items == () and 'node_label_items' in saved_filter_params: node_label_items = saved_filter_params['node_label_items'] except SavedFilter.DoesNotExist: # filter_id not found pass except Exception as inst: @@ -170,8 +171,9 @@ class ExportTopoToXML(BaseViewSet, ViewSet): group_id=group_id, straight_cables=straight_cables, grid_size=grid_size, + node_label_items=node_label_items, ) - xml_data = export_data_to_xml(topo_data).decode('utf-8') + xml_data = export_data_to_xml(topo_data).decode('utf-8').replace('\n', ' ') return HttpResponse(xml_data, content_type="application/xml; charset=utf-8") else: diff --git a/netbox_topology_views/choices.py b/netbox_topology_views/choices.py new file mode 100644 index 0000000..0e0ba7f --- /dev/null +++ b/netbox_topology_views/choices.py @@ -0,0 +1,14 @@ +from utilities.choices import ChoiceSet + +class NodeLabelItems(ChoiceSet): + DEVICE_NAME = 'devicename' + PRIMARY_IPV4 = 'primaryipv4' + PRIMARY_IPV6 = 'primaryipv6' + OUT_OF_BAND_IP = 'outofbandip' + + CHOICES = [ + (DEVICE_NAME, 'Device Name'), + (PRIMARY_IPV4, 'Primary IPv4'), + (PRIMARY_IPV6, 'Primary IPv6'), + (OUT_OF_BAND_IP, 'Out-of-band IP'), + ] diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index aeb8203..9e9e0b4 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -23,6 +23,7 @@ from utilities.forms.fields import ( ) from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate +from netbox_topology_views.choices import NodeLabelItems class DeviceFilterForm( LocalConfigContextFilterForm, @@ -36,7 +37,7 @@ class DeviceFilterForm( FieldSet( 'group', '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', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options") + 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', 'node_label_items', name=_("Options") ), FieldSet('id', name=_("Device")), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")), @@ -335,6 +336,11 @@ class DeviceFilterForm( max_value=1000, help_text=_('Show grid and snap dragged icons to grid. Set to 0 to disable grid and snapping.') ) + node_label_items = forms.MultipleChoiceField( + label=_('Node Label Items'), + required=False, + choices=NodeLabelItems + ) class CoordinateGroupsForm(NetBoxModelForm): fieldsets = ( @@ -530,6 +536,7 @@ class IndividualOptionsForm(NetBoxModelForm): 'draw_default_layout', 'straight_cables', 'grid_size', + 'node_label_items', ), ) @@ -681,6 +688,12 @@ class IndividualOptionsForm(NetBoxModelForm): 'Integers between 0 and 1000 are allowed. Snap to grid will be ' 'automatically enabled for values > 0.') ) + node_label_items = forms.MultipleChoiceField( + label=_('Node Label Items'), + required=False, + choices=NodeLabelItems, + help_text=_('Choose Label Items that you want to be displayed.') + ) class Meta: model = IndividualOptions @@ -689,5 +702,5 @@ class IndividualOptionsForm(NetBoxModelForm): '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', 'group_virtualchassis', 'draw_default_layout', - 'straight_cables', 'grid_size' + 'straight_cables', 'grid_size', 'node_label_items' ] diff --git a/netbox_topology_views/migrations/0011_individualoptions_node_label_items.py b/netbox_topology_views/migrations/0011_individualoptions_node_label_items.py new file mode 100644 index 0000000..7495a02 --- /dev/null +++ b/netbox_topology_views/migrations/0011_individualoptions_node_label_items.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.9 on 2024-09-27 18:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_topology_views', '0010_individualoptions_grid_size'), + ] + + operations = [ + migrations.AddField( + model_name='individualoptions', + name='node_label_items', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py index 1aa8e4b..68e7368 100644 --- a/netbox_topology_views/models.py +++ b/netbox_topology_views/models.py @@ -25,6 +25,7 @@ from netbox_topology_views.utils import ( image_static_url, ) +from netbox_topology_views.choices import NodeLabelItems class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin): class Meta: @@ -407,7 +408,11 @@ class IndividualOptions(NetBoxModel): grid_size = models.PositiveSmallIntegerField( default=0 ) - + node_label_items = models.CharField( + max_length=255, + blank=True + ) + _netbox_private = True def __str___(self): diff --git a/netbox_topology_views/utils.py b/netbox_topology_views/utils.py index f4ed8f4..e0edccc 100644 --- a/netbox_topology_views/utils.py +++ b/netbox_topology_views/utils.py @@ -200,7 +200,11 @@ def get_query_settings(request): if "grid_size" in request.GET: grid_size = request.GET.getlist('grid_size') - return filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size + node_label_items = () + if "node_label_items" in request.GET: + node_label_items = request.GET.getlist('node_label_items') + + return filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size, node_label_items class LinePattern(): wireless = [2, 10, 2, 10] diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index f69a047..5b6f601 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -78,7 +78,7 @@ from netbox_topology_views.utils import ( IMAGE_FILETYPES ) - +from netbox_topology_views.choices import NodeLabelItems def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): is_device = isinstance(entity, Device) @@ -97,7 +97,10 @@ 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, + node_label_items: list, + group_id="default" ): node = {} node_content = "" @@ -229,7 +232,22 @@ def create_node( dev_title = " %s
" % (node_content) node["title"] = dev_title node["name"] = dev_name - node["label"] = dev_name + + # Create a list of possible label items. Omit None types + label_mapping = { + NodeLabelItems.DEVICE_NAME: dev_name, + NodeLabelItems.PRIMARY_IPV4: device.primary_ip4, + NodeLabelItems.PRIMARY_IPV6: device.primary_ip6, + NodeLabelItems.OUT_OF_BAND_IP: device.oob_ip, + } + + label_items = [] + for item in node_label_items: + if label_mapping[item] is not None: + label_items.append(str(label_mapping[item])) + node_label = '\n'.join(label_items) + + node["label"] = node_label node["shape"] = "image" node["href"] = device.get_absolute_url() node["image"] = get_image_for_entity(device) @@ -350,6 +368,7 @@ def get_topology_data( group_id, straight_cables: bool, grid_size: list, + node_label_items: list, ): supported_termination_types = [] @@ -698,7 +717,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, node_label_items, group_id)) results["nodes"] = nodes results["edges"] = edges @@ -729,7 +748,7 @@ class TopologyHomeView(PermissionRequiredMixin, View): if request.GET: - filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = get_query_settings(request) + filter_id, ignore_cable_type, 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, group_virtualchassis, group, show_neighbors, straight_cables, grid_size, node_label_items = 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"] != '': @@ -753,6 +772,7 @@ class TopologyHomeView(PermissionRequiredMixin, View): if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] if straight_cables == False and 'straight_cables' in saved_filter_params: straight_cables = saved_filter_params['straight_cables'] if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size'] + if node_label_items == () and 'node_label_items' in saved_filter_params: node_label_items = saved_filter_params['node_label_items'] except SavedFilter.DoesNotExist: # filter_id not found pass except Exception as inst: @@ -787,6 +807,7 @@ class TopologyHomeView(PermissionRequiredMixin, View): group_id=group_id, straight_cables=straight_cables, grid_size=grid_size, + node_label_items=node_label_items, ) else: @@ -816,6 +837,10 @@ class TopologyHomeView(PermissionRequiredMixin, View): if individualOptions.group_virtualchassis: q['group_virtualchassis'] = "True" if individualOptions.straight_cables: q['straight_cables'] = "True" if individualOptions.grid_size: q['grid_size'] = individualOptions.grid_size + node_label_items = IndividualOptions.objects.get(id=individualOptions.id).node_label_items.translate({ord(i): None for i in '[]\''}).split(', ') + if node_label_items == ['']: node_label_items = [] + q.setlist("node_label_items", node_label_items) + if individualOptions.draw_default_layout: q['draw_init'] = "True" else: @@ -1177,6 +1202,7 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View): 'draw_default_layout': queryset.draw_default_layout, 'straight_cables': queryset.straight_cables, 'grid_size': queryset.grid_size, + 'node_label_items': tuple(queryset.node_label_items.translate({ord(i): None for i in '[]\''}).split(', ')), }, )