Closes #252,#78,#177,#265: Move options from config file to GUI (#262)

* Added some individual options

* Added general options

* Added show_single_cable_logical_conns and draw_default_layout

* Added ignore_cable_type to IndividualOptions

* Corrected permission, tiny improvements

* Add preselected_device_roles to IndividualOptions

* Add preselected_tags

* Updated documentation

* Update README.md

* Corrected queryset for preselected_tags

* Update README.md

* move supported_termination_types to inside the function to prevent migration issues

* removed general options

* added new migration script

---------

Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
This commit is contained in:
Mario
2023-03-23 09:57:51 +01:00
committed by GitHub
co-authored by Mattijs Vanhaverbeke
parent 4fbc2fd8ce
commit 5d20a7ad15
10 changed files with 396 additions and 127 deletions
+28 -23
View File
@@ -67,14 +67,17 @@ You might also set the `always_save_coordinates` option to True.
## Configure ## Configure
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`. All individual options can be assigned a default value per user directly in the plugin. The default value can be overridden on the filter page.
The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`.
Example: Example:
``` ```
PLUGINS_CONFIG = { PLUGINS_CONFIG = {
'netbox_topology_views': { 'netbox_topology_views': {
'preselected_device_roles': ['Router', 'Firewall'], 'static_image_directory': 'netbox_topology_views/img',
'hide_single_cable_logical_conns': True 'allow_coordinates_saving': True,
'always_save_coordinates': True
} }
} }
``` ```
@@ -82,15 +85,8 @@ PLUGINS_CONFIG = {
| Setting | Default value | Description | | Setting | Default value | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| static_image_directory | netbox_topology_views/img | (str or pathlib.Path) Specifies the location that images will be loaded from by default. Must be within `STATIC_ROOT` | | static_image_directory | netbox_topology_views/img | (str or pathlib.Path) Specifies the location that images will be loaded from by default. Must be within `STATIC_ROOT` |
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive |
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates | | allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
| always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default | | always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default |
| 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 ### Custom Images
@@ -102,27 +98,27 @@ Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plu
Select your options for the topology view: Select your options for the topology view:
![preview image](doc/img/selection_options.png?raw=true "preview") ![selection_options](https://user-images.githubusercontent.com/20901110/220410144-5633d24c-6383-4cba-8d6d-ba77ef3d3cdd.png)
<dl> <dl>
<dt>Hide Unconnected</dt>
<dd>Hide devices which have no connections.</dd>
<dt>Save Coordinates</dt> <dt>Save Coordinates</dt>
<dd>Save the coordinates of devices in the topology view.</dd> <dd>Save the coordinates of devices in the topology view.</dd>
<dd>Please read the "Configure" chapter to set the allow_coordinates_saving option to True.</dd> <dt>Show Unconnected</dt>
<dd>Show devices that have no connections or for which no connection is displayed. This option depends on other parameters like 'Show Cables' and 'Show Logical Connections'.</dd>
<dt>Show Circuit Terminations</dt>
<dd>Show connections which end at a circuit termination in the topology view. These connections are displayed as blue dashed lines.</dd>
<dt>Show Cables</dt> <dt>Show Cables</dt>
<dd>Show cable connections between devices, including cables connected to interfaces, front / rear ports, etc., in the topology view</dd> <dd>Show connections between interfaces, front / rear ports, etc., that are connected with one or more cables. These connections are displayed as solid lines in the color of the cable.</dd>
<dt>Show Logical Connections</dt> <dt>Show Logical Connections</dt>
<dd>Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between <dd>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 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. </dd> intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions. These connections are displayed as yellow dotted lines.</dd>
<dd><i>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.</i></dd> <dt>Show redundant Cable and Locigal Connection</dt>
<dt>Show Circuit Terminations</dt> <dd>Shows a logical connection (in addition to a cable), even if a cable is directly connected. Leaving this option disabled prevents that redundant display. This option only has an effect if 'Show Logical Connections' is activated.</dd>
<dd>Show connections which end at a circuit termination in the topology view.</dd>
<dt>Show Wireless Links</dt>
<dd>Show wireless connections in the topology view.</dd>
<dt>Show Power Feeds</dt> <dt>Show Power Feeds</dt>
<dd>Show power connections from power feeds in the topology view.</dd> <dd>Displays connections between power outlets and power ports. These connections are displayed as solid lines in the color of the cable. This option depends on 'Show Cables'.</dd>
<dt>Show Wireless Links</dt>
<dd>Displays wireless connections. These connections are displayed as blue dotted lines.</dd>
</dl> </dl>
### Update ### Update
@@ -139,12 +135,21 @@ Clear you browser cache.
### Permissions ### Permissions
To view `/plugins/topology-views/` you need the following permissions: To view `/plugins/netbox_topology-views/topology` you need the following permissions:
+ dcim | device | can view device + dcim | device | can view device
+ dcim | site | can view site + dcim | site | can view site
+ extras | tag | can view tag + extras | tag | can view tag
+ dcim | device role | can view device role + dcim | device role | can view device role
To view `/plugins/netbox_topology-views/image`:
+ dcim | site | view
+ dcim | device role | view
+ dcim | device role | add
+ dcim | device role | change
To view `/plugins/netbox_topology-views/individualoptions`:
+ netbox topology views | individualoptions | change
## Icons info ## Icons info
Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power). Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power).
+1 -16
View File
@@ -11,25 +11,10 @@ class TopologyViewsConfig(PluginConfig):
base_url = "netbox_topology_views" base_url = "netbox_topology_views"
required_settings = [] required_settings = []
default_settings = { default_settings = {
"preselected_device_roles": [
"Firewall",
"Router",
"Distribution Switch",
"Core Switch",
"Internal Switch",
"Access Switch",
"Server",
"Storage",
"Backup",
"Wireless AP",
],
"ignore_cable_type": [],
"static_image_directory": "netbox_topology_views/img", "static_image_directory": "netbox_topology_views/img",
"allow_coordinates_saving": False, "allow_coordinates_saving": False,
"always_save_coordinates": False, "always_save_coordinates": False,
"preselected_tags": [],
"draw_default_layout": False,
"hide_single_cable_logical_conns": False,
} }
+7 -1
View File
@@ -1,7 +1,8 @@
from dcim.models import Device, DeviceRole from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage from netbox_topology_views.models import RoleImage, IndividualOptions
class TopologyDummySerializer(ModelSerializer): class TopologyDummySerializer(ModelSerializer):
@@ -20,3 +21,8 @@ class DeviceRoleSerializer(ModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ("name", "slug", "color", "vm_role", "description") fields = ("name", "slug", "color", "vm_role", "description")
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("ignore_cable_type", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
+134 -15
View File
@@ -12,17 +12,14 @@ from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm
from django.conf import settings from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms import ( from utilities.forms import (
TagFilterField, TagFilterField,
DynamicModelMultipleChoiceField, DynamicModelMultipleChoiceField,
MultipleChoiceField, MultipleChoiceField,
widgets,
) )
from .models import IndividualOptions
allow_coordinates_saving = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
)
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device model = Device
@@ -32,11 +29,12 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
( (
"q", "q",
"filter_id", "filter_id",
"hide_unconnected",
"save_coords", "save_coords",
"show_unconnected",
"show_cables", "show_cables",
"show_circuit", "show_circuit",
"show_logical_connections", "show_logical_connections",
"show_single_cable_logical_conns",
"show_power", "show_power",
"show_wireless", "show_wireless",
), ),
@@ -104,12 +102,27 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
}, },
label=_("Rack"), label=_("Rack"),
) )
hide_unconnected = forms.BooleanField( status = MultipleChoiceField(
label=_("Hide Unconnected"), required=False, initial=False choices=DeviceStatusChoices, required=False, label=_("Device Status")
)
tag = TagFilterField(model)
# options
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]),
initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"])
)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"), required=False, initial=False
) )
show_logical_connections = forms.BooleanField( show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), required=False, initial=False label =_("Show Logical Connections"), required=False, initial=False
) )
show_single_cable_logical_conns = forms.BooleanField(
label =_("Show redundant Cable and Locigal Connection"), required=False, initial=False
)
show_cables = forms.BooleanField( show_cables = forms.BooleanField(
label =_("Show Cables"), required=False, initial=False label =_("Show Cables"), required=False, initial=False
) )
@@ -122,12 +135,118 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
show_power = forms.BooleanField( show_power = forms.BooleanField(
label=_("Show Power Feeds"), required=False, initial=False label=_("Show Power Feeds"), required=False, initial=False
) )
save_coords = forms.BooleanField(
label=_("Save Coordinates"), class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
(
None,
(
"user_id",
"ignore_cable_type",
"preselected_device_roles",
"preselected_tags",
"show_unconnected",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_single_cable_logical_conns",
"show_power",
"show_wireless",
"draw_default_layout",
),
),
)
user_id = forms.CharField(widget=forms.HiddenInput())
ignore_cable_type = MultipleChoiceField(
label=_("Ignore Termination Types"),
required=False, required=False,
disabled=(not allow_coordinates_saving), choices=IndividualOptions.CHOICES,
help_text=_("Choose Termination Types that you want to be ignored. "
"If any ignored Termination Type is part of a connection, the "
"cable is not displayed.")
) )
status = MultipleChoiceField( preselected_device_roles = DynamicModelMultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status") label=_("Preselected Device Role"),
queryset=DeviceRole.objects.all(),
required=False,
help_text=_("Select Device Roles that you want to have "
"preselected in the filter tab.")
) )
tag = TagFilterField(model) preselected_tags = forms.ModelMultipleChoiceField(
label=_("Preselected Tags"),
queryset=Device.tags.all(),
required=False,
widget=widgets.StaticSelectMultiple,
help_text=_("Select Tags that you want to have "
"preselected in the filter tab.")
)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"),
required=False,
initial=False,
help_text=_("Draws devices that have no connections or for which no "
"connection is displayed. This option depends on other parameters "
"like 'Show Cables' and 'Show Logical Connections'.")
)
show_cables = forms.BooleanField(
label =_("Show Cables"),
required=False,
initial=False,
help_text=_("Displays connections between interfaces that are connected "
"with one or more cables. These connections are displayed as solid "
"lines in the color of the cable.")
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"),
required=False,
initial=False,
help_text=_("Displays connections between devices that are not "
"directly connected (e.g. via patch panels). These connections "
"are displayed as yellow dotted lines.")
)
show_single_cable_logical_conns = forms.BooleanField(
label = ("Show redundant Cable and Locigal Connection"),
required = False,
initial=False,
help_text=_("Shows a logical connection (in addition to a cable), "
"even if a cable is directly connected. Leaving this option "
"disabled prevents that redundant display. This option only "
"has an effect if 'Show Logical Connections' is activated.")
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"),
required=False,
initial=False,
help_text=_("Displays connections between circuit terminations. "
"These connections are displayed as blue dashed lines.")
)
show_power = forms.BooleanField(
label=_("Show Power Feeds"),
required=False,
initial=False,
help_text=_("Displays connections between power outlets and power "
"ports. These connections are displayed as solid lines in the "
"color of the cable. This option depends on 'Show Cables'.")
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"),
required=False,
initial=False,
help_text=_("Displays wireless connections. These connections are "
"displayed as blue dotted lines.")
)
draw_default_layout = forms.BooleanField(
label = ("Draw Default Layout"),
required=False,
initial=False,
help_text=_("Enable this option if you want to draw the topology on "
"the initial load (when you go to the topology plugin page).")
)
class Meta:
model = IndividualOptions
fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_circuit', 'show_power', 'show_wireless', 'draw_default_layout'
]
@@ -0,0 +1,42 @@
# Generated by Django 4.1.4 on 2023-03-19 14:13
from django.db import migrations, models
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0167_module_status'),
('extras', '0084_staging'),
('netbox_topology_views', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='IndividualOptions',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('user_id', models.IntegerField(null=True, unique=True)),
('ignore_cable_type', models.CharField(blank=True, max_length=255)),
('show_unconnected', models.BooleanField(default=False)),
('show_cables', models.BooleanField(default=False)),
('show_logical_connections', models.BooleanField(default=False)),
('show_single_cable_logical_conns', models.BooleanField(default=False)),
('show_circuit', models.BooleanField(default=False)),
('show_power', models.BooleanField(default=False)),
('show_wireless', models.BooleanField(default=False)),
('draw_default_layout', models.BooleanField(default=False)),
('preselected_device_roles', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_device', related_name='+', to='dcim.devicerole')),
('preselected_tags', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_tag', related_name='+', to='extras.tag')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'abstract': False,
},
),
]
+61
View File
@@ -2,10 +2,12 @@ from pathlib import Path
from typing import Optional from typing import Optional
from dcim.models import DeviceRole from dcim.models import DeviceRole
from extras.models import Tag
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.templatetags.static import static from django.templatetags.static import static
from netbox.models import NetBoxModel
from netbox.models.features import ( from netbox.models.features import (
ChangeLoggingMixin, ChangeLoggingMixin,
ExportTemplatesMixin, ExportTemplatesMixin,
@@ -91,3 +93,62 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
except ValueError: except ValueError:
return self.get_default_image(dir) return self.get_default_image(dir)
return static(f"/{self.image}") return static(f"/{self.image}")
class IndividualOptions(NetBoxModel):
CHOICES = (
('interface', 'interface'),
('front port', 'front port'),
('rear port', 'rear port'),
('power outlet', 'power outlet'),
('power port', 'power port'),
('console port', 'console port'),
('console server port', 'console server port'),
)
user_id = models.IntegerField(
null=True,
unique=True
)
ignore_cable_type = models.CharField(
max_length = 255,
blank = True,
)
preselected_device_roles = models.ManyToManyField(
to='dcim.DeviceRole',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_device',
)
preselected_tags = models.ManyToManyField(
to='extras.Tag',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_tag',
)
show_unconnected = models.BooleanField(
default=False
)
show_cables = models.BooleanField(
default=False
)
show_logical_connections = models.BooleanField(
default=False
)
show_single_cable_logical_conns = models.BooleanField(
default=False
)
show_circuit = models.BooleanField(
default=False
)
show_power = models.BooleanField(
default=False
)
show_wireless = models.BooleanField(
default=False
)
draw_default_layout = models.BooleanField(
default=False
)
def __str___(self):
return f"{self.user_id}"
+7 -2
View File
@@ -4,7 +4,12 @@ menu = PluginMenu(
label='Topology Views', label='Topology Views',
icon_class="mdi mdi-sitemap", icon_class="mdi mdi-sitemap",
groups=( groups=(
('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),), ('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('CUSTOMIZATION', (PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Topology Images"),),), ('PREFERENCES',
(
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options"),
),
),
), ),
) )
@@ -0,0 +1,5 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Topology Views Individual Options{% endblock %}
{% block tabs %} {% endblock tabs %}
+1
View File
@@ -9,4 +9,5 @@ urlpatterns = (
path("", RedirectView.as_view(url="topology/", permanent=True)), path("", RedirectView.as_view(url="topology/", permanent=True)),
path("topology/", views.TopologyHomeView.as_view(), name="home"), path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"), path("images/", views.TopologyImagesView.as_view(), name="images"),
path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"),
) )
+110 -70
View File
@@ -18,9 +18,11 @@ from dcim.models import (
RearPort, RearPort,
) )
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, QuerySet from django.db.models import Q, QuerySet
from django.db.models.functions import Lower
from django.http import HttpRequest, HttpResponseRedirect, QueryDict from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
@@ -28,8 +30,8 @@ from extras.models import Tag
from wireless.models import WirelessLink from wireless.models import WirelessLink
from netbox_topology_views.filters import DeviceFilterSet from netbox_topology_views.filters import DeviceFilterSet
from netbox_topology_views.forms import DeviceFilterForm from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm
from netbox_topology_views.models import RoleImage from netbox_topology_views.models import RoleImage, IndividualOptions
from netbox_topology_views.utils import ( from netbox_topology_views.utils import (
CONF_IMAGE_DIR, CONF_IMAGE_DIR,
find_image_url, find_image_url,
@@ -38,15 +40,6 @@ from netbox_topology_views.utils import (
image_static_url, image_static_url,
) )
supported_termination_types = [
"interface",
"front port",
"rear port",
"power outlet",
"power port",
"console port",
"console server port",
]
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
@@ -261,14 +254,21 @@ def create_circuit_termination(termination):
def get_topology_data( def get_topology_data(
queryset: QuerySet, queryset: QuerySet,
hide_unconnected: bool, individualOptions: IndividualOptions,
show_unconnected: bool,
save_coords: bool, save_coords: bool,
show_cables: bool, show_cables: bool,
show_circuit: bool, show_circuit: bool,
show_logical_connections: bool, show_logical_connections: bool,
show_single_cable_logical_conns: bool,
show_power: bool, show_power: bool,
show_wireless: bool, show_wireless: bool,
): ):
supported_termination_types = []
for t in IndividualOptions.CHOICES:
supported_termination_types.append(t[1])
if not queryset: if not queryset:
return None return None
@@ -282,14 +282,8 @@ def get_topology_data(
nodes_provider_networks = {} nodes_provider_networks = {}
cable_ids = DefaultDict(dict) cable_ids = DefaultDict(dict)
interface_ids = DefaultDict(dict) interface_ids = DefaultDict(dict)
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
"ignore_cable_type" ignore_cable_type = individualOptions.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] device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset] site_ids = [d.site_id for d in queryset]
@@ -301,7 +295,7 @@ def get_topology_data(
for circuit_termination in circuit_terminations: for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination circuit_termination: CircuitTermination
if ( if (
not hide_unconnected show_unconnected
and circuit_termination.circuit_id not in nodes_circuits and circuit_termination.circuit_id not in nodes_circuits
): ):
nodes_circuits[ nodes_circuits[
@@ -358,7 +352,7 @@ def get_topology_data(
if termination.device_id in device_ids: if termination.device_id in device_ids:
circuit_has_connections = True circuit_has_connections = True
if circuit_has_connections and hide_unconnected: if circuit_has_connections and not show_unconnected:
if circuit_termination.circuit_id not in nodes_circuits: if circuit_termination.circuit_id not in nodes_circuits:
nodes_circuits[ nodes_circuits[
circuit_termination.circuit.pk circuit_termination.circuit.pk
@@ -376,15 +370,15 @@ def get_topology_data(
) )
for power_feed in power_feeds: for power_feed in power_feeds:
if not hide_unconnected or ( if show_unconnected or (
hide_unconnected and power_feed.cable_id is not None not show_unconnected and power_feed.cable_id is not None
): ):
if power_feed.power_panel_id not in nodes_powerpanel: if power_feed.power_panel_id not in nodes_powerpanel:
nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel
power_link_name = "" power_link_name = ""
if power_feed.pk not in nodes_powerfeed: if power_feed.pk not in nodes_powerfeed:
if hide_unconnected: if not show_unconnected:
if power_feed.link_peers[0].device_id in device_ids: if power_feed.link_peers[0].device_id in device_ids:
nodes_powerfeed[power_feed.pk] = power_feed nodes_powerfeed[power_feed.pk] = power_feed
power_link_name = power_feed.link_peers[0].name power_link_name = power_feed.link_peers[0].name
@@ -438,7 +432,7 @@ def get_topology_data(
# print('Destination interface already exists, ignoring') # print('Destination interface already exists, ignoring')
continue continue
if hide_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables: if not show_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 # interface connection is the same as the cable connection, ignore this connection
continue continue
@@ -569,7 +563,7 @@ def get_topology_data(
) )
for qs_device in queryset: for qs_device in queryset:
if qs_device.pk not in nodes_devices and not hide_unconnected: if qs_device.pk not in nodes_devices and show_unconnected:
nodes_devices[qs_device.pk] = qs_device nodes_devices[qs_device.pk] = qs_device
results = {} results = {}
@@ -598,16 +592,27 @@ class TopologyHomeView(PermissionRequiredMixin, View):
self.model = self.queryset.model self.model = self.queryset.model
topo_data = None topo_data = None
individualOptions, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
if request.GET: if request.GET:
save_coords = False save_coords = False
if "save_coords" in request.GET: if "save_coords" in request.GET:
if request.GET["save_coords"] == "on": if request.GET["save_coords"] == "on":
save_coords = True save_coords = True
# General options overrides
if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False:
save_coords = False
messages.warning(request, "Coordinate saving not allowed. Setting has been overridden")
elif settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"] == True:
save_coords = True
hide_unconnected = False # Individual options
if "hide_unconnected" in request.GET: show_unconnected = False
if request.GET["hide_unconnected"] == "on": if "show_unconnected" in request.GET:
hide_unconnected = True if request.GET["show_unconnected"] == "on":
show_unconnected = True
show_power = False show_power = False
if "show_power" in request.GET: if "show_power" in request.GET:
@@ -624,6 +629,11 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET["show_logical_connections"] == "on" : if request.GET["show_logical_connections"] == "on" :
show_logical_connections = True show_logical_connections = True
show_single_cable_logical_conns = False
if "show_single_cable_logical_conns" in request.GET:
if request.GET["show_single_cable_logical_conns"] == "on" :
show_single_cable_logical_conns = True
show_cables = False show_cables = False
if "show_cables" in request.GET: if "show_cables" in request.GET:
if request.GET["show_cables"] == "on" : if request.GET["show_cables"] == "on" :
@@ -634,60 +644,45 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET["show_wireless"] == "on" : if request.GET["show_wireless"] == "on" :
show_wireless = True show_wireless = True
if "draw_init" in request.GET: if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data( topo_data = get_topology_data(
self.queryset, self.queryset,
hide_unconnected, individualOptions,
show_unconnected,
save_coords, save_coords,
show_cables, show_cables,
show_circuit, show_circuit,
show_logical_connections, show_logical_connections,
show_single_cable_logical_conns,
show_power, show_power,
show_wireless, 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"][
"preselected_device_roles"
]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"][
"preselected_tags"
]
always_save_coordinates = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"][
"always_save_coordinates"
]
)
q_device_role_id = DeviceRole.objects.filter( else:
name__in=preselected_device_roles # No GET-Request in URL. We most likely came here from the navigation menu.
).values_list("id", flat=True) preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list( preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
"name", flat=True
)
q = QueryDict(mutable=True) q = QueryDict(mutable=True)
q.setlist("device_role_id", list(q_device_role_id)) q.setlist("device_role_id", list(preselected_device_roles))
q.setlist("tag", list(q_tags)) q.setlist("tag", list(preselected_tags))
q["draw_init"] = settings.PLUGINS_CONFIG["netbox_topology_views"][
"draw_default_layout" if individualOptions.show_unconnected: q['show_unconnected'] = "on"
] if individualOptions.show_cables: q['show_cables'] = "on"
if always_save_coordinates: if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
q["save_coords"] = "on" if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
if individualOptions.show_circuit: q['show_circuit'] = "on"
if individualOptions.show_power: q['show_power'] = "on"
if individualOptions.show_wireless: q['show_wireless'] = "on"
if individualOptions.draw_default_layout:
q['draw_init'] = "true"
else:
q['draw_init'] = "false"
query_string = q.urlencode() query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}") return HttpResponseRedirect(f"{request.path}?{query_string}")
if is_htmx(request): if is_htmx(request):
return render( return render(
request, request,
@@ -768,3 +763,48 @@ class TopologyImagesView(PermissionRequiredMixin, View):
"images": images, "images": images,
}, },
) )
class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
permission_required = 'netbox_topology_views.change_individualoptions'
def post(self, request):
instance = IndividualOptions.objects.get(user_id=request.user.id)
form = IndividualOptionsForm(request.POST, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Options have been sucessfully saved")
else:
messages.error(request, form.errors)
return HttpResponseRedirect("./")
def get(self, request):
queryset, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
form = IndividualOptionsForm(
initial={
'user_id': request.user.id,
'ignore_cable_type': tuple(queryset.ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')),
'preselected_device_roles': IndividualOptions.objects.get(id=queryset.id).preselected_device_roles.all(),
'preselected_tags': IndividualOptions.objects.get(id=queryset.id).preselected_tags.all(),
'show_unconnected': queryset.show_unconnected,
'show_cables': queryset.show_cables,
'show_logical_connections': queryset.show_logical_connections,
'show_single_cable_logical_conns': queryset.show_single_cable_logical_conns,
'show_circuit': queryset.show_circuit,
'show_power': queryset.show_power,
'show_wireless': queryset.show_wireless,
'draw_default_layout': queryset.draw_default_layout,
},
)
return render(
request,
"netbox_topology_views/individual_options.html",
{
"form": form,
"object": queryset,
},
)