Compare commits
62
Commits
v3.6.1
...
dreng-patch-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8f5d728c | ||
|
|
6eb20004f9 | ||
|
|
836d9f40d7 | ||
|
|
607347ff18 | ||
|
|
0f6051731a | ||
|
|
3c9aebfd2f | ||
|
|
60a5fe1eae | ||
|
|
3fbeb97810 | ||
|
|
f3b822e18d | ||
|
|
555c1fac4c | ||
|
|
cf8641939a | ||
|
|
2893fff111 | ||
|
|
25fdcb6cf4 | ||
|
|
5d330bc830 | ||
|
|
03b8c9c76e | ||
|
|
02a7118a33 | ||
|
|
44e49495fe | ||
|
|
f4b1e34826 | ||
|
|
f8c6e9e193 | ||
|
|
19adcab4a9 | ||
|
|
d2d2106246 | ||
|
|
9072faf7cf | ||
|
|
6f43b959bd | ||
|
|
1048feff52 | ||
|
|
7ff0a9650f | ||
|
|
8d5de5c49f | ||
|
|
96afdf8d83 | ||
|
|
aba0598947 | ||
|
|
142de460b9 | ||
|
|
2600209724 | ||
|
|
ed8a717bc5 | ||
|
|
0b7046b10d | ||
|
|
e784e717ca | ||
|
|
2895294903 | ||
|
|
8368ffa2fd | ||
|
|
23428c8ad2 | ||
|
|
c00b8c2cfe | ||
|
|
32003e59f9 | ||
|
|
bd32642b5c | ||
|
|
f463804087 | ||
|
|
b92b012309 | ||
|
|
74c295837c | ||
|
|
a543f5cb0c | ||
|
|
50c90a4328 | ||
|
|
374a88266a | ||
|
|
60cbc94a62 | ||
|
|
649305c83e | ||
|
|
b3397f4f4a | ||
|
|
a23604261a | ||
|
|
d9571b30b5 | ||
|
|
76b43f5d1e | ||
|
|
becfaaa054 | ||
|
|
bec301eb9d | ||
|
|
f28623b378 | ||
|
|
16837047be | ||
|
|
987c7ef34c | ||
|
|
dbe331afb8 | ||
|
|
86c0c55896 | ||
|
|
78b8a5ae97 | ||
|
|
71e4807cb1 | ||
|
|
dfbe26318f | ||
|
|
debc314714 |
@@ -0,0 +1,15 @@
|
||||
name: NetBox plugin manifest modified
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- netbox-plugin.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
manifest-modified:
|
||||
uses: netboxlabs/public-workflows/.github/workflows/reusable-plugin-manifest-modified.yml@release
|
||||
@@ -14,31 +14,15 @@ Options to export to xml (for draw.io/diagrams.net) or png.
|
||||
|
||||
## Install
|
||||
|
||||
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
|
||||
**_NOTE:_** For Docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
|
||||
|
||||
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder
|
||||
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder.
|
||||
|
||||
The plugin is available as a Python package and can be installed with pip.
|
||||
|
||||
Run `pip install netbox-topology-views` in your virtual env.
|
||||
|
||||
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
||||
|
||||
```no-highlight
|
||||
# echo netbox-topology-views >> local_requirements.txt
|
||||
```
|
||||
|
||||
Once installed, the plugin needs to be enabled in your `configuration.py`
|
||||
|
||||
```python
|
||||
# In your configuration.py
|
||||
PLUGINS = ["netbox_topology_views"]
|
||||
```
|
||||
|
||||
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
|
||||
|
||||
|
||||
Then run
|
||||
Then run:
|
||||
```bash
|
||||
cd /opt/netbox/netbox
|
||||
pip3 install netbox-topology-views
|
||||
@@ -46,18 +30,40 @@ python3 manage.py migrate netbox_topology_views
|
||||
python3 manage.py collectstatic --no-input
|
||||
```
|
||||
|
||||
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
||||
|
||||
```no-highlight
|
||||
echo netbox-topology-views >> local_requirements.txt
|
||||
```
|
||||
|
||||
Once installed, the plugin needs to be enabled in your `configuration.py`:
|
||||
|
||||
```python
|
||||
PLUGINS = ["netbox_topology_views"]
|
||||
```
|
||||
|
||||
Finally, restart NetBox:
|
||||
```no-highlight
|
||||
systemctl restart netbox
|
||||
```
|
||||
|
||||
### Versions
|
||||
|
||||
> [!NOTE]
|
||||
> If the version of netbox is not listed here the plugin will most likely not work. Remove the plugin if you want to update netbox before this plugin has released an update for that netbox version.
|
||||
|
||||
| netbox version | netbox-topology-views version |
|
||||
| -------------- | ----------------------------- |
|
||||
| >= 3.5.0 | >= v3.4.X |
|
||||
| >= 3.4.0 | >= v3.X.X |
|
||||
| >= 3.3.0 | >= v3.0.0 |
|
||||
| >= 3.2.0 | >= v1.1.0 |
|
||||
| >= 3.1.8 | >= v1.0.0 |
|
||||
| >= 2.11.1 | >= v0.5.3 |
|
||||
| >= 2.10.0 | >= v0.5.0 |
|
||||
| < 2.10.0 | =< v0.4.10 |
|
||||
| = 4.3.X | = v4.2.1 |
|
||||
| = 4.2.1 | = v4.2.1 |
|
||||
| = 4.2.0 | = v4.2.0 |
|
||||
| = 4.1.X | = v4.1.0 |
|
||||
| = 4.0.X | = v4.0.0 |
|
||||
| = 3.7.0 | = v3.9.0 |
|
||||
| = 3.6.4 | = v3.8.1 |
|
||||
| = 3.6.0 | = v3.7.0 |
|
||||
| = 3.5.0 | = v3.6.2 |
|
||||
|
||||
|
||||
### Update
|
||||
|
||||
@@ -199,3 +205,12 @@ Set `Coordinate Groups` according to your needs:
|
||||
|
||||
Set `Coordinates` according to your needs:
|
||||
+ netbox_topology_views | coordinate | view/add/change/delete
|
||||
|
||||
Set `Power Feed Coordinates` according to your needs:
|
||||
+ netbox_topology_views | power feed coordinate | view/add/change/delete
|
||||
|
||||
Set `Power Panel Coordinates` according to your needs:
|
||||
+ netbox_topology_views | power panel coordinate | view/add/change/delete
|
||||
|
||||
Set `Circuit Coordinates` according to your needs:
|
||||
+ netbox_topology_views | circuit coordinate | view/add/change/delete
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.4'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
|
||||
POSTGRES_DB: netbox
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
- netbox-postgres-data-16:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- "6378:6379"
|
||||
|
||||
volumes:
|
||||
netbox-postgres-data:
|
||||
netbox-postgres-data-16:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
version: 0.1
|
||||
package_name: netbox-topology-views
|
||||
compatibility:
|
||||
- release: 4.2.0
|
||||
netbox_min: 4.2.0
|
||||
netbox_max: 4.2.1
|
||||
- release: 4.1.0
|
||||
netbox_min: 4.1.0
|
||||
netbox_max: 4.1.11
|
||||
- release: 4.0.1
|
||||
netbox_min: 4.0.9
|
||||
netbox_max: 4.0.11
|
||||
- release: 4.0.0
|
||||
netbox_min: 4.0.0
|
||||
netbox_max: 4.0.9
|
||||
- release: 3.9.1
|
||||
netbox_min: 3.7.1
|
||||
netbox_max: 3.7.8
|
||||
- release: 3.9.0
|
||||
netbox_min: 3.7.1
|
||||
netbox_max: 3.7.8
|
||||
- release: 3.8.1
|
||||
netbox_min: 3.6.9
|
||||
netbox_max: 3.6.9
|
||||
- release: 3.6.2
|
||||
netbox_min: 3.5.1
|
||||
netbox_max: 3.5.9
|
||||
@@ -1,11 +1,11 @@
|
||||
from extras.plugins import PluginConfig
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
|
||||
class TopologyViewsConfig(PluginConfig):
|
||||
name = "netbox_topology_views"
|
||||
verbose_name = "Topology views"
|
||||
description = "An plugin to render topology maps"
|
||||
version = "3.6.1"
|
||||
version = "4.2.1"
|
||||
author = "Mattijs Vanhaverbeke"
|
||||
author_email = "author@example.com"
|
||||
base_url = "netbox_topology_views"
|
||||
|
||||
@@ -2,7 +2,7 @@ from dcim.models import Device, DeviceRole
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
|
||||
class TopologyDummySerializer(ModelSerializer):
|
||||
@@ -32,7 +32,22 @@ class CoordinateSerializer(NetBoxModelSerializer):
|
||||
model = Coordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class CircuitCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class PowerPanelCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
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", "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", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size", "node_label_items")
|
||||
|
||||
@@ -6,6 +6,11 @@ router = NetBoxRouter()
|
||||
|
||||
router.register("save-coords", views.SaveCoordsViewSet)
|
||||
router.register("images", views.SaveRoleImageViewSet)
|
||||
router.register("xml-export", views.ExportTopoToXML)
|
||||
router.register("xml-export", views.ExportTopoToXML, "xml-export")
|
||||
router.register("coordinate-groups", views.CoordinateGroupViewSet)
|
||||
router.register("coordinate", views.CoordinateViewSet)
|
||||
router.register("circuitcoordinate", views.CircuitCoordinateViewSet)
|
||||
router.register("powerpanelcoordinate", views.PowerPanelCoordinateViewSet)
|
||||
router.register("powerfeedcoordinate", views.PowerFeedCoordinateViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from typing import Dict
|
||||
import sys
|
||||
|
||||
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||
from extras.models import SavedFilter
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -13,13 +17,19 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
from netbox_topology_views.api.serializers import (
|
||||
RoleImageSerializer,
|
||||
TopologyDummySerializer,
|
||||
CoordinateGroupSerializer,
|
||||
CoordinateSerializer,
|
||||
CircuitCoordinateSerializer,
|
||||
PowerPanelCoordinateSerializer,
|
||||
PowerFeedCoordinateSerializer,
|
||||
)
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
|
||||
import netbox_topology_views.models
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
from netbox_topology_views.views import get_topology_data
|
||||
from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings
|
||||
from netbox_topology_views.filters import DeviceFilterSet
|
||||
|
||||
class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
class SaveCoordsViewSet(BaseViewSet, ReadOnlyModelViewSet):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = Device.objects.none()
|
||||
@@ -41,20 +51,26 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
if device_id.startswith("c"):
|
||||
device_id = device_id.lstrip("c")
|
||||
actual_device = Circuit.objects.get(id=device_id)
|
||||
model_name = 'CircuitCoordinate'
|
||||
elif device_id.startswith("p"):
|
||||
device_id = device_id.lstrip("p")
|
||||
actual_device = PowerPanel.objects.get(id=device_id)
|
||||
model_name = 'PowerPanelCoordinate'
|
||||
elif device_id.startswith("f"):
|
||||
device_id = device_id.lstrip("f")
|
||||
actual_device = PowerFeed.objects.get(id=device_id)
|
||||
model_name = 'PowerFeedCoordinate'
|
||||
elif device_id.isnumeric():
|
||||
actual_device = Device.objects.get(id=device_id)
|
||||
model_name = 'Coordinate'
|
||||
|
||||
if not actual_device:
|
||||
return Response({"status": "invalid node_id in body"}, status=400)
|
||||
|
||||
model_class = getattr(netbox_topology_views.models, model_name)
|
||||
|
||||
if group_id is None or group_id == "default":
|
||||
group_id = Coordinate.get_or_create_default_group(group_id)
|
||||
group_id = model_class.get_or_create_default_group(group_id)
|
||||
if not group_id:
|
||||
return Response(
|
||||
{"status": "Error while creating default group."}, status=500
|
||||
@@ -66,12 +82,12 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
# Hen-and-egg-problem. Thanks, Django! By default, Django updates records that
|
||||
# already exist and inserts otherwise. This does not work with our
|
||||
# unique_together key if no pk is given. But: No record, no pk.
|
||||
if not Coordinate.objects.filter(group=group, device=actual_device):
|
||||
if not model_class.objects.filter(group=group, device=actual_device):
|
||||
# Unique group/device pair does not exist. Prepare new data set
|
||||
coords = Coordinate(group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords = model_class(group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
else:
|
||||
# Unique group/device pair already exists. Update data
|
||||
coords = Coordinate(pk=Coordinate.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords = model_class(pk=model_class.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords.save()
|
||||
except:
|
||||
return Response(
|
||||
@@ -80,17 +96,15 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
|
||||
return Response({"status": "saved coords"})
|
||||
|
||||
class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
||||
permission_required = ("dcim.view_site", "dcim.view_device")
|
||||
|
||||
class ExportTopoToXML(BaseViewSet, ViewSet):
|
||||
queryset = Device.objects.none()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
def list(self, request):
|
||||
|
||||
self.filterset = DeviceFilterSet
|
||||
self.queryset = Device.objects.all().select_related(
|
||||
"device_type", "device_role"
|
||||
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
|
||||
"device_type", "role"
|
||||
)
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
|
||||
@@ -100,14 +114,47 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
||||
|
||||
if request.GET:
|
||||
|
||||
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = 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"] != '':
|
||||
try:
|
||||
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||
saved_filter_params = getattr(saved_filter, 'parameters')
|
||||
|
||||
if ignore_cable_type == () and 'ignore_cable_type' in saved_filter_params: ignore_cable_type = saved_filter_params['ignore_cable_type']
|
||||
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
|
||||
if show_unconnected == False and 'show_unconnected' in saved_filter_params: show_unconnected = saved_filter_params['show_unconnected']
|
||||
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
|
||||
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
|
||||
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
|
||||
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
|
||||
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
|
||||
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
||||
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 group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
|
||||
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:
|
||||
print(type(inst))
|
||||
|
||||
if 'group' not in request.query_params:
|
||||
group_id = "default"
|
||||
if 'saved_filter_params' in locals() and "group" in saved_filter_params:
|
||||
group_id = saved_filter_params['group'][0]
|
||||
else:
|
||||
group_id = "default"
|
||||
else:
|
||||
group_id = request.query_params["group"]
|
||||
topo_data = get_topology_data(
|
||||
queryset=self.queryset,
|
||||
individualOptions=individualOptions,
|
||||
ignore_cable_type=ignore_cable_type,
|
||||
save_coords=save_coords,
|
||||
show_unconnected=show_unconnected,
|
||||
show_cables=show_cables,
|
||||
@@ -117,9 +164,16 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
||||
show_circuit=show_circuit,
|
||||
show_power=show_power,
|
||||
show_wireless=show_wireless,
|
||||
group_sites=group_sites,
|
||||
group_locations=group_locations,
|
||||
group_racks=group_racks,
|
||||
group_virtualchassis=group_virtualchassis,
|
||||
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:
|
||||
@@ -127,12 +181,12 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
||||
{"status": "Missing or malformed request parameters"}, status=400
|
||||
)
|
||||
|
||||
class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
class SaveRoleImageViewSet(BaseViewSet, ReadOnlyModelViewSet):
|
||||
queryset = DeviceRole.objects.none()
|
||||
serializer_class = RoleImageSerializer
|
||||
permission_required = (
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
"dcim.add_devicerole",
|
||||
"dcim.change_devicerole",
|
||||
)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
@@ -142,15 +196,26 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
{"status": "Missing or malformed request body"}, status=400
|
||||
)
|
||||
|
||||
device_roles = {
|
||||
k: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.isnumeric()}
|
||||
content_type_ids = {
|
||||
k[2:]: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.startswith("ct") and k[2:].isnumeric()
|
||||
}
|
||||
if sys.version_info >= (3,9,0):
|
||||
device_roles = {
|
||||
k: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.isnumeric()}
|
||||
content_type_ids = {
|
||||
k[2:]: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.startswith("ct") and k[2:].isnumeric()
|
||||
}
|
||||
else:
|
||||
device_roles = {}
|
||||
for k, v in request.data.items():
|
||||
if k.isdigit() and v.startswith(settings.STATIC_URL):
|
||||
device_roles[k] = v[len(settings.STATIC_URL):]
|
||||
|
||||
content_type_ids = {}
|
||||
for k, v in request.data.items():
|
||||
if k.startswith("ct") and k[2:].isdigit() and v.startswith(settings.STATIC_URL):
|
||||
content_type_ids[k[2:]] = v[len(settings.STATIC_URL):]
|
||||
|
||||
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
|
||||
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
|
||||
@@ -194,3 +259,23 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
)
|
||||
|
||||
return JsonResponse({"status": "Ok"})
|
||||
|
||||
class CoordinateGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = CoordinateGroup.objects.all()
|
||||
serializer_class = CoordinateGroupSerializer
|
||||
|
||||
class CoordinateViewSet(NetBoxModelViewSet):
|
||||
queryset = Coordinate.objects.all()
|
||||
serializer_class = CoordinateSerializer
|
||||
|
||||
class CircuitCoordinateViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
serializer_class = CircuitCoordinateSerializer
|
||||
|
||||
class PowerPanelCoordinateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
serializer_class = PowerPanelCoordinateSerializer
|
||||
|
||||
class PowerFeedCoordinateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
serializer_class = PowerFeedCoordinateSerializer
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
@@ -1,28 +1,56 @@
|
||||
import django_filters
|
||||
from circuits.models import Circuit
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site
|
||||
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site, SiteGroup, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
|
||||
from django.db.models import Q
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter, MultiValueCharFilter, MultiValueMACAddressFilter
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name="device_role_id",
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label="Manufacturer (ID)",
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label="Manufacturer (slug)",
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
label="Device type (ID)",
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name="role_id",
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label="Role (ID)",
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label="Platform (ID)",
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name="site__region",
|
||||
lookup_expr="in",
|
||||
label="Region (ID)",
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name="site__group",
|
||||
lookup_expr="in",
|
||||
label="Site Group (ID)",
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label="Site (ID)",
|
||||
@@ -42,10 +70,57 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
choices=DeviceStatusChoices,
|
||||
null_value=None,
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
field_name='interfaces__mac_addresses__mac_address',
|
||||
label="MAC address",
|
||||
)
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
console_ports = django_filters.BooleanFilter(
|
||||
method='_console_ports',
|
||||
label="Has console ports",
|
||||
)
|
||||
console_server_ports = django_filters.BooleanFilter(
|
||||
method='_console_server_ports',
|
||||
label="Has console server ports",
|
||||
)
|
||||
power_ports = django_filters.BooleanFilter(
|
||||
method='_power_ports',
|
||||
label="Has power ports",
|
||||
)
|
||||
power_outlets = django_filters.BooleanFilter(
|
||||
method='_power_outlets',
|
||||
label="Has power outlets",
|
||||
)
|
||||
interfaces = django_filters.BooleanFilter(
|
||||
method='_interfaces',
|
||||
label="Has interfaces",
|
||||
)
|
||||
pass_through_ports = django_filters.BooleanFilter(
|
||||
method='_pass_through_ports',
|
||||
label="Has pass-through ports",
|
||||
)
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
label="Config template (ID)",
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label="Has a primary IP",
|
||||
)
|
||||
has_oob_ip = django_filters.BooleanFilter(
|
||||
method='_has_oob_ip',
|
||||
label="Has an out-of-band IP",
|
||||
)
|
||||
virtual_chassis_member = django_filters.BooleanFilter(
|
||||
method='_virtual_chassis_member',
|
||||
label="Is a virtual chassis member",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ["id", "name"]
|
||||
fields = ["id", "name", "asset_tag", "airflow"]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -54,6 +129,108 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
qs_filter = Q(name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
return queryset.exclude(consoleports__isnull=value)
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
return queryset.exclude(consoleserverports__isnull=value)
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
return queryset.exclude(powerports__isnull=value)
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
return queryset.exclude(poweroutlets__isnull=value)
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
return queryset.exclude(interfaces__isnull=value)
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
return queryset.exclude(
|
||||
frontports__isnull=value,
|
||||
rearports__isnull=value
|
||||
)
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
if value:
|
||||
return queryset.filter(params)
|
||||
return queryset.exclude(params)
|
||||
|
||||
def _has_oob_ip(self, queryset, name, value):
|
||||
params = Q(oob_ip__isnull=False)
|
||||
if value:
|
||||
return queryset.filter(params)
|
||||
return queryset.exclude(params)
|
||||
|
||||
def _virtual_chassis_member(self, queryset, name, value):
|
||||
return queryset.exclude(virtual_chassis__isnull=value)
|
||||
|
||||
class CircuitCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = Circuit.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class PowerPanelCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = PowerPanel.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class PowerFeedCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = PowerFeed.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class CoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
|
||||
+562
-193
@@ -1,157 +1,350 @@
|
||||
from cProfile import label
|
||||
import imp
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device, Site, Region, DeviceRole, Location, Rack
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Device, Site, SiteGroup, Region, DeviceRole, Location, Rack, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
|
||||
|
||||
from django import forms
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.choices import DeviceStatusChoices, DeviceAirflowChoices
|
||||
from tenancy.models import TenantGroup, Tenant
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from extras.models import ConfigTemplate
|
||||
from extras.forms import LocalConfigContextFilterForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from django.conf import settings
|
||||
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField
|
||||
)
|
||||
|
||||
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
from netbox_topology_views.choices import NodeLabelItems
|
||||
|
||||
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class DeviceFilterForm(
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
ContactModelFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"q",
|
||||
"filter_id",
|
||||
),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
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', 'node_label_items', name=_("Options")
|
||||
),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"group",
|
||||
"save_coords",
|
||||
"show_unconnected",
|
||||
"show_cables",
|
||||
"show_logical_connections",
|
||||
"show_single_cable_logical_conns",
|
||||
"show_neighbors",
|
||||
"show_circuit",
|
||||
"show_power",
|
||||
"show_wireless",
|
||||
),
|
||||
FieldSet('id', name=_("Device")),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
|
||||
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_("Operation")),
|
||||
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_("Hardware")),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_("Tenant")),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_("Contacts")),
|
||||
FieldSet(
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', name=_("Components")
|
||||
),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"tenant_group_id",
|
||||
"tenant_id",
|
||||
),
|
||||
FieldSet(
|
||||
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', name=_("Miscellaneous")
|
||||
),
|
||||
(None, ("region_id", "site_id", "location_id", "rack_id")),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"device_role_id",
|
||||
"id",
|
||||
"status",
|
||||
),
|
||||
),
|
||||
(None, ("tag",)),
|
||||
)
|
||||
group = forms.ModelChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False,
|
||||
label=_("Coordinate group"),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(), required=False, label=_("Region")
|
||||
)
|
||||
device_role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
|
||||
label=_('Coordinate group'),
|
||||
)
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_("Device"),
|
||||
label=_('Device'),
|
||||
query_params={
|
||||
"location_id": "$location_id",
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
"role_id": "$device_role_id",
|
||||
'location_id': '$location_id',
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
'role_id': '$role_id',
|
||||
'contact': '$contact',
|
||||
'contact_role': '$contact_role',
|
||||
'contact_group': '$contact_group',
|
||||
},
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site Group'),
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_("Site"),
|
||||
label=_('Site'),
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_("Location"),
|
||||
label=_('Location'),
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
"location_id": "$location_id",
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_("Rack"),
|
||||
label=_('Rack'),
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices, required=False, label=_("Device Status")
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
label=_('Device Status')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Role')
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
serial = forms.CharField(
|
||||
label=_('Serial'),
|
||||
required=False
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
label=_('Asset tag'),
|
||||
required=False
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label=_('MAC address')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer_id'
|
||||
},
|
||||
label=_('Model')
|
||||
)
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Platform')
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has console ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
console_server_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has console server ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
power_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has power ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
power_outlets = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has power outlets'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
interfaces = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has interfaces'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
pass_through_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has pass-through ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
config_template_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False,
|
||||
label=_('Config template')
|
||||
)
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has a primary IP'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
has_oob_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has an OOB IP'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
virtual_chassis_member = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Virtual chassis member'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
# options
|
||||
save_coords = forms.BooleanField(
|
||||
label=_("Save Coordinates"),
|
||||
ignore_cable_type = forms.MultipleChoiceField(
|
||||
label=_('Ignore Termination Types'),
|
||||
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"])
|
||||
choices=IndividualOptions.CHOICES
|
||||
)
|
||||
show_unconnected = forms.BooleanField(
|
||||
label=_("Show Unconnected"), required=False, initial=False
|
||||
save_coords = forms.NullBooleanField(
|
||||
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']),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_cables = forms.BooleanField(
|
||||
label =_("Show Cables"), required=False, initial=False
|
||||
show_unconnected = forms.NullBooleanField(
|
||||
label=_('Show Unconnected'),
|
||||
required=False,
|
||||
initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_logical_connections = forms.BooleanField(
|
||||
label =_("Show Logical Connections"), required=False, initial=False
|
||||
|
||||
show_cables = forms.NullBooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_('Show Cables'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_single_cable_logical_conns = forms.BooleanField(
|
||||
label =_("Show redundant Cable and Logical Connection"), required=False, initial=False
|
||||
|
||||
show_logical_connections = forms.NullBooleanField(
|
||||
label =_('Show Logical Connections'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_neighbors = forms.BooleanField(
|
||||
label =_("Show Neighbors"), required=False, initial=False
|
||||
show_single_cable_logical_conns = forms.NullBooleanField(
|
||||
label =_('Show redundant Cable and Logical Connection'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_circuit = forms.BooleanField(
|
||||
label=_("Show Circuit Terminations"), required=False, initial=False
|
||||
show_neighbors = forms.NullBooleanField(
|
||||
label =_('Show Neighbors'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_power = forms.BooleanField(
|
||||
label=_("Show Power Feeds"), required=False, initial=False
|
||||
show_circuit = forms.NullBooleanField(
|
||||
label=_('Show Circuit Terminations'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_wireless = forms.BooleanField(
|
||||
label =_("Show Wireless Links"), required=False, initial=False
|
||||
show_power = forms.NullBooleanField(
|
||||
label=_('Show Power Feeds'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_wireless = forms.NullBooleanField(
|
||||
label =_('Show Wireless Links'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_sites = forms.NullBooleanField(
|
||||
label =_('Group Sites'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_locations = forms.NullBooleanField(
|
||||
label =_('Group Locations'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_racks = forms.NullBooleanField(
|
||||
label =_('Group Racks'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_virtualchassis = forms.NullBooleanField(
|
||||
label =_('Group Virtual Chassis'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
straight_cables = forms.BooleanField(
|
||||
label=_('Straight Cables'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
grid_size = forms.IntegerField(
|
||||
label=_('Grid Size'),
|
||||
required=False,
|
||||
initial=0,
|
||||
min_value=0,
|
||||
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 = (
|
||||
('Group Details', ('name', 'description')),
|
||||
FieldSet('name', 'description', name=_("Group Details")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -163,25 +356,142 @@ class CoordinateGroupsImportForm(NetBoxModelImportForm):
|
||||
model = CoordinateGroup
|
||||
fields = ('name', 'description')
|
||||
|
||||
class CircuitCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Power Panel")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
('Coordinate', ('group', 'device', 'x', 'y')),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Coordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CircuitCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Coordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CircuitCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitCoordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinates")),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class PowerPanelCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = PowerPanelCoordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_('PowerPanel Coordinates')),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class PowerFeedCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Coordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinates")),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class CoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Coordinate
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
('Coordinates', ('group', 'device', 'x', 'y'))
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Coordinates")),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
@@ -204,133 +514,192 @@ class CoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class IndividualOptionsForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
FieldSet
|
||||
(
|
||||
"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",
|
||||
"draw_default_layout",
|
||||
'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',
|
||||
'group_virtualchassis',
|
||||
'draw_default_layout',
|
||||
'straight_cables',
|
||||
'grid_size',
|
||||
'node_label_items',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
user_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
ignore_cable_type = forms.MultipleChoiceField(
|
||||
label=_("Ignore Termination Types"),
|
||||
required=False,
|
||||
label=_('Ignore Termination Types'),
|
||||
required=False,
|
||||
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.")
|
||||
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.')
|
||||
)
|
||||
preselected_device_roles = DynamicModelMultipleChoiceField(
|
||||
label=_("Preselected Device Role"),
|
||||
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.")
|
||||
help_text=_('Select Device Roles that you want to have '
|
||||
'preselected in the filter tab.')
|
||||
)
|
||||
preselected_tags = forms.ModelMultipleChoiceField(
|
||||
label=_("Preselected Tags"),
|
||||
label=_('Preselected Tags'),
|
||||
queryset=Device.tags.all(),
|
||||
required=False,
|
||||
help_text=_("Select Tags that you want to have "
|
||||
"preselected in the filter tab.")
|
||||
help_text=_('Select Tags that you want to have '
|
||||
'preselected in the filter tab.')
|
||||
)
|
||||
save_coords = forms.BooleanField(
|
||||
label=_("Save Coordinates"),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_("Coordinates of nodes will be saved if dragged to a different "
|
||||
"position. This option depends on parameters set in the config file. "
|
||||
"It has no effect if 'allow_coordinates_saving' has not been set or "
|
||||
" 'always_save_coordinates' has been set.")
|
||||
)
|
||||
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 Logical 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_neighbors = forms.BooleanField(
|
||||
label =_("Show Neighbors"),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_("Adds neighbors to the filter result set automatically. "
|
||||
"Link peers will be added if 'Show Cables' is ticked, far-end "
|
||||
"terminations will be added if 'Show Logical Connections' is ticked.")
|
||||
)
|
||||
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"),
|
||||
label=_('Save Coordinates'),
|
||||
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).")
|
||||
help_text=_('Coordinates of nodes will be saved if dragged to a different '
|
||||
'position. This option depends on parameters set in the config file. '
|
||||
'It has no effect if \'allow_coordinates_saving\' has not been set or '
|
||||
' \'always_save_coordinates\' has been set.')
|
||||
)
|
||||
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 Logical 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_neighbors = forms.BooleanField(
|
||||
label =_('Show Neighbors'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Adds neighbors to the filter result set automatically. '
|
||||
'Link peers will be added if \'Show Cables\' is ticked, far-end '
|
||||
'terminations will be added if \'Show Logical Connections\' is ticked.')
|
||||
)
|
||||
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.')
|
||||
)
|
||||
group_sites = forms.BooleanField(
|
||||
label =_('Group Sites'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Draws a rectangle around Devices that belong to the '
|
||||
'same site.')
|
||||
)
|
||||
group_locations = forms.BooleanField(
|
||||
label =_('Group Locations'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Draws a rectangle around Devices that belong to the '
|
||||
'same location.')
|
||||
)
|
||||
group_racks = forms.BooleanField(
|
||||
label =_('Group Racks'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Draws a rectangle around Devices that belong to the '
|
||||
'same rack.')
|
||||
)
|
||||
group_virtualchassis = forms.BooleanField(
|
||||
label =_('Group Virtual Chassis'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Draws a rectangle around Devices that belong to the '
|
||||
'same virtual chassis.')
|
||||
)
|
||||
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).')
|
||||
)
|
||||
straight_cables = forms.BooleanField(
|
||||
label=('Use Straight Cables'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Enable this option if you want to draw cables as straight lines '
|
||||
'instead of curves.')
|
||||
)
|
||||
grid_size = forms.IntegerField(
|
||||
label=_('Grid Size'),
|
||||
required=True,
|
||||
initial=0,
|
||||
min_value=0,
|
||||
max_value=1000,
|
||||
help_text=_('Default grid value. Set to 0 to disable grid. '
|
||||
'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
|
||||
fields = [
|
||||
'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', 'draw_default_layout'
|
||||
]
|
||||
'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', 'group_virtualchassis', 'draw_default_layout',
|
||||
'straight_cables', 'grid_size', 'node_label_items'
|
||||
]
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 4.1.8 on 2023-09-25 21:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_topology_views', '0005_individualoptions_save_coords'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PowerPanelCoordinate',
|
||||
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)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerpanel')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerFeedCoordinate',
|
||||
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)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerfeed')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitCoordinate',
|
||||
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)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='circuits.circuit')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-02 16:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_topology_views', '0006_powerpanelcoordinate_powerfeedcoordinate_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='individualoptions',
|
||||
name='group_locations',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='individualoptions',
|
||||
name='group_racks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='individualoptions',
|
||||
name='group_sites',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -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='straight_cables',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2024-07-29 08:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_topology_views', '0008_straight_cables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='individualoptions',
|
||||
name='group_virtualchassis',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2024-08-14 09:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_topology_views', '0009_individualoptions_group_virtualchassis'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='individualoptions',
|
||||
name='grid_size',
|
||||
field=models.PositiveSmallIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+196
-11
@@ -1,7 +1,8 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dcim.models import Device, DeviceRole
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Device, DeviceRole, PowerPanel, PowerFeed
|
||||
from extras.models import Tag
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -12,7 +13,7 @@ from netbox.models import NetBoxModel
|
||||
from netbox.models.features import (
|
||||
ChangeLoggingMixin,
|
||||
ExportTemplatesMixin,
|
||||
WebhooksMixin,
|
||||
EventRulesMixin,
|
||||
)
|
||||
|
||||
from netbox_topology_views.utils import (
|
||||
@@ -24,8 +25,9 @@ from netbox_topology_views.utils import (
|
||||
image_static_url,
|
||||
)
|
||||
|
||||
from netbox_topology_views.choices import NodeLabelItems
|
||||
|
||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
@@ -40,6 +42,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||
|
||||
__role: Optional[Role] = None
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
@property
|
||||
def role(self) -> Role:
|
||||
if self.__role:
|
||||
@@ -110,6 +114,8 @@ class CoordinateGroup(NetBoxModel):
|
||||
blank = True,
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -130,13 +136,15 @@ class Coordinate(NetBoxModel):
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
def get_or_create_default_group(group_id):
|
||||
# Default group named "default" must always exist in order to make sure
|
||||
# that coordinate values can be stored even if no coordinate group has been
|
||||
@@ -168,15 +176,168 @@ class Coordinate(NetBoxModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])
|
||||
|
||||
class CircuitCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(Circuit, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
def get_or_create_default_group(group_id):
|
||||
# Default group named "default" must always exist in order to make sure
|
||||
# that coordinate values can be stored even if no coordinate group has been
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:circuitcoordinate', args=[self.pk])
|
||||
|
||||
class PowerPanelCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(PowerPanel, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
def get_or_create_default_group(group_id):
|
||||
# Default group named "default" must always exist in order to make sure
|
||||
# that coordinate values can be stored even if no coordinate group has been
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:powerpanelcoordinate', args=[self.pk])
|
||||
|
||||
class PowerFeedCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(PowerFeed, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
def get_or_create_default_group(group_id):
|
||||
# Default group named "default" must always exist in order to make sure
|
||||
# that coordinate values can be stored even if no coordinate group has been
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:powerfeedcoordinate', args=[self.pk])
|
||||
|
||||
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'),
|
||||
('frontport', 'frontport'),
|
||||
('rearport', 'rearport'),
|
||||
('poweroutlet', 'poweroutlet'),
|
||||
('powerport', 'powerport'),
|
||||
('consoleport', 'consoleport'),
|
||||
('consoleserverport', 'consoleserverport'),
|
||||
)
|
||||
|
||||
user_id = models.IntegerField(
|
||||
@@ -226,9 +387,33 @@ class IndividualOptions(NetBoxModel):
|
||||
show_wireless = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
group_sites = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
group_locations = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
group_racks = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
group_virtualchassis = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
draw_default_layout = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
straight_cables = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
grid_size = models.PositiveSmallIntegerField(
|
||||
default=0
|
||||
)
|
||||
node_label_items = models.CharField(
|
||||
max_length=255,
|
||||
blank=True
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
def __str___(self):
|
||||
return f"{self.user_id}"
|
||||
|
||||
@@ -1,18 +1,62 @@
|
||||
from extras.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
|
||||
from utilities.choices import ButtonColorChoices
|
||||
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
|
||||
|
||||
coordinategroup_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:coordinategroup_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
color=ButtonColorChoices.GREEN,
|
||||
permissions=['netbox_topology_views.add_coordinategroup']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:coordinategroup_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
color=ButtonColorChoices.CYAN,
|
||||
permissions=['netbox_topology_views.add_coordinategroup']
|
||||
)
|
||||
)
|
||||
|
||||
circuitcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:circuitcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:circuitcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
powerpanelcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerpanelcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerpanelcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
powerfeedcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerfeedcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerfeedcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
@@ -21,13 +65,13 @@ coordinate_buttons = (
|
||||
link='plugins:netbox_topology_views:coordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
color=ButtonColorChoices.GREEN,
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:coordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
color=ButtonColorChoices.CYAN,
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
@@ -37,15 +81,22 @@ menu = PluginMenu(
|
||||
groups=(
|
||||
('TOPOLOGY',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Coordinates", buttons=coordinate_buttons),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology", permissions=["dcim.view_site", "dcim.view_device"]),
|
||||
),
|
||||
),
|
||||
('COORDINATES',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons, permissions=['netbox_topology_views.view_coordinategroup']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Device Coordinates", buttons=coordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:powerfeedcoordinate_list", link_text="Power Feed Coords", buttons=powerfeedcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:powerpanelcoordinate_list", link_text="Power Panel Coords", buttons=powerpanelcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:circuitcoordinate_list", link_text="Circuit Coordinates", buttons=circuitcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
),
|
||||
),
|
||||
('PREFERENCES',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options"),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images", permissions=[ "dcim.view_site","dcim.view_devicerole"]),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options", permissions=['netbox_topology_views.change_individualoptions']),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1 +1 @@
|
||||
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
|
||||
#visgraph{height:64vh;border:1px solid #ced4da}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529;border:1px solid #495057}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,13 @@
|
||||
#visgraph {
|
||||
height: 64vh;
|
||||
border: 1px solid #ced4da
|
||||
}
|
||||
|
||||
html[data-netbox-color-mode=dark] #visgraph {
|
||||
background-color: #212529;
|
||||
border: 1px solid #495057
|
||||
}
|
||||
|
||||
|
||||
.image-dropdown img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
@@ -69,17 +69,160 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
title: htmlTitle(node.title)
|
||||
}))
|
||||
)
|
||||
|
||||
const group_sites = topologyData.options.group_sites
|
||||
const group_locations = topologyData.options.group_locations
|
||||
const group_racks = topologyData.options.group_racks
|
||||
const group_virtualchassis = topologyData.options.group_virtualchassis
|
||||
|
||||
const gridSize = parseInt(topologyData.options.grid_size[0]);
|
||||
var dragMode = false;
|
||||
|
||||
graph = new Network(container, { nodes, edges }, options)
|
||||
graph.fit()
|
||||
|
||||
function getGridPosition(nodeId, gridSize) {
|
||||
x = graph.getPosition(nodeId).x;
|
||||
y = graph.getPosition(nodeId).y;
|
||||
|
||||
if(x >= 0) {
|
||||
if((x % gridSize) > (gridSize / 2)) {
|
||||
x += gridSize;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if((-x % gridSize) > (gridSize / 2)) {
|
||||
x -= gridSize;
|
||||
}
|
||||
}
|
||||
x = x - x % gridSize;
|
||||
|
||||
if(y >= 0) {
|
||||
if((y % gridSize) > (gridSize / 2)) {
|
||||
y += gridSize;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if((-y % gridSize) > (gridSize / 2)) {
|
||||
y -= gridSize;
|
||||
}
|
||||
}
|
||||
y = y - y % gridSize;
|
||||
|
||||
return {
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
}
|
||||
|
||||
function drawGrid(canvascontext) {
|
||||
// Canvas can be zoomed. It then contains more or less virtual pixels than the real number of pixels
|
||||
const zoomFactor = graph.getScale() * window.devicePixelRatio;
|
||||
const virtualWidth = canvascontext.canvas.width / zoomFactor;
|
||||
const virtualHeight = canvascontext.canvas.height / zoomFactor;
|
||||
|
||||
// Canvas can be moved. Get the center of the virtual canvas. Take the grid into account
|
||||
const virtualCenter = graph.getViewPosition();
|
||||
const rasterizedCenterX = virtualCenter.x - virtualCenter.x % gridSize;
|
||||
const rasterizedCenterY = virtualCenter.y - virtualCenter.y % gridSize;
|
||||
|
||||
// Calculate virtual space for the grid
|
||||
const hSpace = (virtualWidth / 2) - (virtualWidth / 2) % gridSize + gridSize;
|
||||
const vSpace = (virtualHeight / 2) - (virtualHeight / 2) % gridSize + gridSize;
|
||||
|
||||
// Calculate virtual position for the grid
|
||||
const left = rasterizedCenterX - gridSize - hSpace;
|
||||
const right = rasterizedCenterX + gridSize + hSpace;
|
||||
const top = rasterizedCenterY - gridSize - vSpace;
|
||||
const bottom = rasterizedCenterY + gridSize + vSpace;
|
||||
|
||||
// Draw grid
|
||||
canvascontext.beginPath();
|
||||
|
||||
for (let x = left; x < right; x += gridSize) {
|
||||
canvascontext.moveTo(x, top);
|
||||
canvascontext.lineTo(x, bottom);
|
||||
}
|
||||
|
||||
for (let y = top; y < bottom; y += gridSize) {
|
||||
canvascontext.moveTo(left, y);
|
||||
canvascontext.lineTo(right, y);
|
||||
}
|
||||
|
||||
canvascontext.strokeStyle = '#777777';
|
||||
canvascontext.stroke();
|
||||
}
|
||||
|
||||
function drawGridSnapHint(canvascontext) {
|
||||
// Draw grid hinting line and circle
|
||||
if(gridSize > 0 && dragMode == true && graph.getSelectedNodes().length > 0) {
|
||||
for(i = 0; i < graph.getSelectedNodes().length; i++) {
|
||||
id = graph.getSelectedNodes()[i];
|
||||
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
|
||||
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
|
||||
|
||||
canvascontext.beginPath();
|
||||
canvascontext.arc(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y, 5, 0, 2 * Math.PI);
|
||||
canvascontext.fillStyle = '#FF3D3D';
|
||||
canvascontext.fill();
|
||||
|
||||
canvascontext.beginPath();
|
||||
canvascontext.moveTo(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y);
|
||||
canvascontext.lineTo(pos.x, pos.y);
|
||||
canvascontext.strokeStyle = '#FF3D3D';
|
||||
canvascontext.stroke();
|
||||
|
||||
canvascontext.beginPath();
|
||||
canvascontext.arc(pos.x, pos.y, 10, 0, 2 * Math.PI);
|
||||
canvascontext.fillStyle = '#9C0000';
|
||||
canvascontext.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph.on('dragStart', (params) => {
|
||||
dragMode = true;
|
||||
})
|
||||
|
||||
graph.on('dragEnd', (params) => {
|
||||
if (coordSaveCheckbox == null) return
|
||||
if (!coordSaveCheckbox.checked) return
|
||||
dragMode = false;
|
||||
// Place icon on the grid
|
||||
if(gridSize > 0 && graph.getSelectedNodes().length > 0) {
|
||||
for(i = 0; i < graph.getSelectedNodes().length; i++) {
|
||||
id = graph.getSelectedNodes()[i];
|
||||
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
|
||||
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
|
||||
window.nodes.update({id: graph.getSelectedNodes()[i], x: pos.x, y: pos.y});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (coordSaveCheckbox.options[coordSaveCheckbox.selectedIndex].text != "Yes") return
|
||||
|
||||
Promise.allSettled(
|
||||
Object.entries(graph.getPositions(params.nodes)).map(
|
||||
async ([nodeId, nodePosition]) => {
|
||||
window.nodes.update({id: parseInt(nodeId), physics: false, x: nodePosition.x, y: nodePosition.y});
|
||||
if(!isNaN(parseInt(nodeId))) {
|
||||
nodeKey = parseInt(nodeId);
|
||||
}
|
||||
else {
|
||||
nodeKey = nodeId;
|
||||
}
|
||||
|
||||
try {
|
||||
window.nodes.update({id: nodeKey, physics: false, x: nodePosition.x, y: nodePosition.y});
|
||||
}
|
||||
catch (e) {
|
||||
console.log([
|
||||
'Error while executing window.nodes.update()',
|
||||
'nodeId: ' + nodeId,
|
||||
'nodeKey: ' + nodeKey,
|
||||
'x: ' + nodePosition.x,
|
||||
'y: ' + nodePosition.y
|
||||
]);
|
||||
console.log(e);
|
||||
}
|
||||
const res = await fetch(
|
||||
'/' + basePath + 'api/plugins/netbox_topology_views/save-coords/save_coords/',
|
||||
{
|
||||
@@ -97,8 +240,6 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log(nodeId, res.status, res.statusText)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -116,6 +257,227 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
graph.on('beforeDrawing', (canvascontext) => {
|
||||
if (gridSize > 0) {
|
||||
drawGrid(canvascontext);
|
||||
}
|
||||
})
|
||||
|
||||
graph.on('afterDrawing', (canvascontext) => {
|
||||
allRectangles = [];
|
||||
if(group_sites != null && group_sites == 'on') { drawGroupRectangles(canvascontext, groupedNodeSites, siteRectParams); }
|
||||
if(group_locations != null && group_locations == 'on') { drawGroupRectangles(canvascontext, groupedNodeLocations, locationRectParams); }
|
||||
if(group_racks != null && group_racks == 'on') { drawGroupRectangles(canvascontext, groupedNodeRacks, rackRectParams); }
|
||||
if(group_virtualchassis != null && group_virtualchassis == 'on') { drawGroupRectangles(canvascontext, groupedNodeVirtualchassis, virtualchassisRectParams); }
|
||||
|
||||
drawGridSnapHint(canvascontext);
|
||||
})
|
||||
|
||||
graph.on('click', (canvascontext) => {
|
||||
allRectangles.forEach(key => {
|
||||
// Is the mouse pointer inside of the current rectangle?
|
||||
if(canvascontext.pointer.canvas.x > (key.x1 - key.border / 2 - 3) && canvascontext.pointer.canvas.x < (key.x2 + key.border / 2 + 3)
|
||||
&& canvascontext.pointer.canvas.y > (key.y1 - key.border / 2 - 3) && canvascontext.pointer.canvas.y < (key.y2 + key.border / 2 + 3)) {
|
||||
// We just want to react when the border has been clicked, not the whole rectangle
|
||||
if (canvascontext.pointer.canvas.x < (key.x1 + key.border / 2 + 3) || canvascontext.pointer.canvas.x > (key.x2 - key.border / 2 - 3)
|
||||
|| canvascontext.pointer.canvas.y < (key.y1 + key.border / 2 + 3) || canvascontext.pointer.canvas.y > (key.y2 - key.border / 2 - 3)) {
|
||||
// Generate an array of affected nodes in order to pass it to the select.Nodes() function
|
||||
let arr = [];
|
||||
if(key.category == "Site") {
|
||||
groupedNodeSites.forEach(subArray => {
|
||||
subArray.forEach(element => {
|
||||
if (element[1] == key.id) {
|
||||
arr.push(element[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if(key.category == "Location") {
|
||||
groupedNodeLocations.forEach(subArray => {
|
||||
subArray.forEach(element => {
|
||||
if (element[1] === key.id) {
|
||||
arr.push(element[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if(key.category == "Rack") {
|
||||
groupedNodeRacks.forEach(subArray => {
|
||||
subArray.forEach(element => {
|
||||
if (element[1] === key.id) {
|
||||
arr.push(element[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if(key.category == "Virtual Chassis") {
|
||||
groupedNodeVirtualchassis.forEach(subArray => {
|
||||
subArray.forEach(element => {
|
||||
if (element[1] === key.id) {
|
||||
arr.push(element[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
graph.selectNodes(arr);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Add information on which node belongs to which group (site/location/rack/virtualchassis).
|
||||
// Create an array for each group in order to loop through that arrays later
|
||||
function combineNodeInfo(typeId, type) {
|
||||
let nodesArray = [];
|
||||
// Extract node ids and node type ids from all nodes
|
||||
for (let [key, value] of nodes._data) {
|
||||
if (value[typeId] != undefined) {
|
||||
nodesArray.push([value.id, value[typeId], value[type], value.label_num_lines]);
|
||||
}
|
||||
}
|
||||
// Split single array above into arrays grouped by node id
|
||||
let groupedNodeArray = nodesArray.reduce((acc, value) => {
|
||||
let key = value[1]; // node id
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(value);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.values(groupedNodeArray);
|
||||
}
|
||||
|
||||
var allRectangles = [];
|
||||
/* Draw a single rectangle with given parameters
|
||||
rectangle expects an object that consists of the following keys:
|
||||
ctx: canvas context on which the rectangle should be drawn
|
||||
x: x-coordinate of top left point of the rectangle
|
||||
y: y-coordinate of top left point of the rectangle
|
||||
width: width of rectangle
|
||||
height: height of rectangle
|
||||
lineWidth: border width
|
||||
color: border color
|
||||
text: a string to be placed where you want it to be
|
||||
textPaddingX: x-position of the text
|
||||
textPaddingY: y-position of the text
|
||||
font: text font */
|
||||
function drawGroupRectangle(rectangle) {
|
||||
// Draw rectangle
|
||||
rectangle.ctx.beginPath();
|
||||
rectangle.ctx.lineWidth = rectangle.lineWidth;
|
||||
rectangle.ctx.strokeStyle = rectangle.color;
|
||||
rectangle.ctx.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
|
||||
rectangle.ctx.stroke();
|
||||
// Draw text
|
||||
rectangle.ctx.font = rectangle.font;
|
||||
rectangle.ctx.fillStyle = rectangle.color;
|
||||
rectangle.ctx.fillText(rectangle.text, rectangle.x + rectangle.textPaddingX, rectangle.y + rectangle.textPaddingY);
|
||||
|
||||
allRectangles.push({category: rectangle.category, id: rectangle.id, x1: rectangle.x, y1: rectangle.y, x2: rectangle.x + rectangle.width, y2: rectangle.y + rectangle.height, border: rectangle.lineWidth})
|
||||
}
|
||||
|
||||
/* Draw all rectangles of a given group (site/location//virtualchassis)
|
||||
rectParams expects an object that consists of the following keys:
|
||||
lineWidth: border width (string)
|
||||
color: border color (string)
|
||||
paddingX: rectangle x-padding, calculated from the center of a node (int)
|
||||
paddingY: rectangle y-padding, calculated from the center of a node (int)
|
||||
textPaddingX: text x-padding, calculated from the lower left point of the text (int)
|
||||
textPaddingY: text y-padding, calculated from the lower left point of the text (int)
|
||||
font: css-like font size and font (string) */
|
||||
function drawGroupRectangles(canvascontext, groupedNodes, rectParams) {
|
||||
for(let value of Object.entries(groupedNodes)) {
|
||||
const rectangles = [];
|
||||
const xValues = [];
|
||||
const yValues = [];
|
||||
const yValuesWithLabel = [];
|
||||
const labelLineHeight = 16;
|
||||
|
||||
// Get coordinates for every node in a given group
|
||||
for(let val of value[1]) {
|
||||
xValues.push(graph.getPosition(val[0]).x);
|
||||
yValues.push(graph.getPosition(val[0]).y);
|
||||
yValuesWithLabel.push(yValues[yValues.length -1] + val[3] * labelLineHeight);
|
||||
}
|
||||
|
||||
const minX = Math.min(...xValues);
|
||||
const maxX = Math.max(...xValues);
|
||||
const minY = Math.min(...yValues);
|
||||
const maxY = Math.max(...yValuesWithLabel);
|
||||
const rectX = minX - rectParams.paddingX;
|
||||
const rectY = minY - rectParams.paddingY;
|
||||
const rectSizeX = maxX - minX + 2*rectParams.paddingX;
|
||||
const rectSizeY = maxY - minY + 2*rectParams.paddingY;
|
||||
|
||||
rectangles.push({
|
||||
ctx: canvascontext,
|
||||
x: rectX,
|
||||
y: rectY,
|
||||
width: rectSizeX,
|
||||
height: rectSizeY,
|
||||
lineWidth: rectParams.lineWidth,
|
||||
color: rectParams.color,
|
||||
text: value[1][0][2],
|
||||
textPaddingX: rectParams.textPaddingX,
|
||||
textPaddingY: rectParams.textPaddingY,
|
||||
font: rectParams.font,
|
||||
id: value[1][0][1],
|
||||
category: rectParams.category
|
||||
});
|
||||
|
||||
rectangles.forEach(function(rectangle) {
|
||||
drawGroupRectangle(rectangle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let groupedNodeSites = combineNodeInfo('site_id', 'site');
|
||||
let siteRectParams = {
|
||||
lineWidth: "5",
|
||||
color: "red",
|
||||
paddingX: 84,
|
||||
paddingY: 84,
|
||||
textPaddingX: 8,
|
||||
textPaddingY: -8,
|
||||
font: "14px helvetica",
|
||||
category: "Site"
|
||||
}
|
||||
|
||||
let groupedNodeLocations = combineNodeInfo('location_id', 'location');
|
||||
let locationRectParams = {
|
||||
lineWidth: "5",
|
||||
color: "#337ab7",
|
||||
paddingX: 77,
|
||||
paddingY: 77,
|
||||
textPaddingX: 22,
|
||||
textPaddingY: 29,
|
||||
font: "14px helvetica",
|
||||
category: "Location"
|
||||
}
|
||||
|
||||
let groupedNodeRacks = combineNodeInfo('rack_id', 'rack');
|
||||
let rackRectParams = {
|
||||
lineWidth: "5",
|
||||
color: "green",
|
||||
paddingX: 70,
|
||||
paddingY: 70,
|
||||
textPaddingX: 15,
|
||||
textPaddingY: 36,
|
||||
font: "14px helvetica",
|
||||
category: "Rack"
|
||||
}
|
||||
|
||||
let groupedNodeVirtualchassis = combineNodeInfo('virtual_chassis_id', 'virtual_chassis');
|
||||
let virtualchassisRectParams = {
|
||||
lineWidth: "5",
|
||||
color: "orange",
|
||||
paddingX: 63,
|
||||
paddingY: 63,
|
||||
textPaddingX: 8,
|
||||
textPaddingY: 43,
|
||||
font: "14px helvetica",
|
||||
category: "Virtual Chassis"
|
||||
}
|
||||
})()
|
||||
|
||||
// Download Graph
|
||||
@@ -157,12 +519,12 @@ function performXmlDownload() {
|
||||
if (curr_url.includes(sites_prefix)) {
|
||||
var site_id = curr_url.split(sites_prefix)[1];
|
||||
site_id = site_id.split('/')[0]
|
||||
xml_search_options = 'site_id=' + site_id + '&show_cables=on&show_unconnected=on'
|
||||
xml_search_options = 'site_id=' + site_id + '&show_cables=True&show_unconnected=True'
|
||||
}
|
||||
else if (curr_url.includes(location_prefix)) {
|
||||
var location_id = curr_url.split(location_prefix)[1];
|
||||
location_id = location_id.split('/')[0]
|
||||
xml_search_options = 'location_id=' + location_id + '&show_cables=on&show_unconnected=on'
|
||||
xml_search_options = 'location_id=' + location_id + '&show_cables=True&show_unconnected=True'
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -191,17 +553,17 @@ const observer = new MutationObserver((mutations) =>
|
||||
if (
|
||||
!graph ||
|
||||
mutation.type !== 'attributes' ||
|
||||
mutation.attributeName !== 'data-netbox-color-mode' ||
|
||||
mutation.attributeName !== 'data-bs-theme' ||
|
||||
!(mutation.target instanceof HTMLElement)
|
||||
)
|
||||
return
|
||||
const { netboxColorMode } = mutation.target.dataset
|
||||
const netboxColorMode = mutation.target.dataset.bsTheme
|
||||
options.nodes.font.color = netboxColorMode === 'dark' ? '#fff' : '#000'
|
||||
graph.setOptions(options)
|
||||
})
|
||||
)
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-netbox-color-mode']
|
||||
attributeFilter: ['data-bs-theme']
|
||||
})
|
||||
|
||||
+1345
-470
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.6.1",
|
||||
"version": "4.2.0",
|
||||
"scripts": {
|
||||
"bundle": "node bundle.js",
|
||||
"bundle:styles": "node bundle.js --styles",
|
||||
"bundle:scripts": "node bundle.js --scripts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.6",
|
||||
"vis-network": "^9.1.6",
|
||||
"vis-util": "^5.0.3"
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vis-util": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild-sass-plugin": "^1.5.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"keycharm": "^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^8.0.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from netbox.tables import NetBoxTable, ChoiceFieldColumn
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
class CoordinateGroupListTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
@@ -14,6 +14,48 @@ class CoordinateGroupListTable(NetBoxTable):
|
||||
fields = ('pk', 'id', 'name', 'description', 'devices')
|
||||
default_columns = ('name', 'description', 'devices')
|
||||
|
||||
class CircuitCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
@@ -27,4 +69,3 @@ class CoordinateListTable(NetBoxTable):
|
||||
model = Coordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from netbox.plugins import PluginTemplateExtension
|
||||
from django.conf import settings
|
||||
from packaging import version
|
||||
|
||||
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
|
||||
NETBOX_CURRENT_VERSION = version.parse(settings.RELEASE.version)
|
||||
|
||||
|
||||
class SiteButtons(PluginTemplateExtension):
|
||||
model = 'dcim.site'
|
||||
models = ('dcim.site', )
|
||||
def buttons(self):
|
||||
return self.render('netbox_topology_views/site_button.html')
|
||||
|
||||
class LocationButtons(PluginTemplateExtension):
|
||||
model = 'dcim.location'
|
||||
models = ('dcim.location', )
|
||||
def buttons(self):
|
||||
return self.render('netbox_topology_views/location_button.html')
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuit</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Circuit Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Circuit Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Circuit Coordinates{% endblock title %}
|
||||
@@ -32,10 +32,34 @@
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Coordinates</h5>
|
||||
<h5 class="card-header">Circuit Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table circuitcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Power Panel Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table powerpanelcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Power Feed Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table powerfeedcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Device Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table coordinates_table %}
|
||||
</div>
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
<a id="btnDownloadXml" class="btn btn-sm btn-info" href="#">
|
||||
<a id="btnDownloadXml" class="btn btn-info" href="#">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download XML
|
||||
</a>
|
||||
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
|
||||
<a id="btnDownloadImage" class="btn btn-info" href="#">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download PNG
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" data-bs-dismiss="modal">
|
||||
<a class="btn btn-danger" data-bs-dismiss="modal">
|
||||
<i class="mdi mdi-close-thick"></i>
|
||||
Close
|
||||
</a>
|
||||
@@ -40,5 +40,6 @@
|
||||
var is_htmx = true;
|
||||
var brokenImage = '{{ broken_image }}';
|
||||
var topologyData = {{ topology_data | safe }};
|
||||
const basePath = '{{ basepath }}';
|
||||
</script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
{% block content %}
|
||||
<form id="images" class="container py-4">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3 mb-2">
|
||||
{% for role in roles %}
|
||||
@@ -39,7 +39,7 @@
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
</form>
|
||||
{% include 'netbox_topology_views/toasts.html' %}
|
||||
{% endblock content-wrapper %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% extends 'generic/_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
@@ -16,11 +16,11 @@
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
<a id="btnDownloadXml" class="btn btn-sm btn-info" href="#">
|
||||
<a id="btnDownloadXml" class="btn btn-info" href="#">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download XML
|
||||
</a>
|
||||
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
|
||||
<a id="btnDownloadImage" class="btn btn-info" href="#">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download PNG
|
||||
</a>
|
||||
@@ -29,7 +29,7 @@
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<ul class="nav nav-tabs" id="tabswrapper">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
|
||||
@@ -48,13 +48,15 @@
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-content" id="tabcontentwrapper">
|
||||
|
||||
{# Applied filters #}
|
||||
{% if filter_form %}
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
<div id="filterwrapper">
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||
@@ -71,13 +73,42 @@
|
||||
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock content-wrapper %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
const brokenImage = '{{ broken_image }}';
|
||||
const topologyData = {{ topology_data | safe }};
|
||||
const basePath = '{{ basepath }}';
|
||||
</script>
|
||||
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
function resizeCanvas() {
|
||||
heightToSubtract = getElementHeight('.page-header') +
|
||||
getElementHeight('#tabswrapper') +
|
||||
getElementHeight('#filterwrapper') +
|
||||
getElementHeight('.footer') +
|
||||
getElementPadding('#tabcontentwrapper');
|
||||
|
||||
document.getElementById("visgraph").style.height = "calc(100vh - " + heightToSubtract + "px)";
|
||||
}
|
||||
|
||||
function getElementPadding(element) {
|
||||
el = document.querySelector(element);
|
||||
els = getComputedStyle(el);
|
||||
padding = parseInt(els.paddingTop) + parseInt(els.paddingBottom);
|
||||
|
||||
return padding;
|
||||
}
|
||||
|
||||
function getElementHeight(element) {
|
||||
el = document.querySelector(element);
|
||||
els = getComputedStyle(el);
|
||||
height = el.offsetHeight + parseInt(els.marginTop) + parseInt(els.marginBottom);
|
||||
|
||||
return height;
|
||||
}
|
||||
</script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
|
||||
{% endblock javascript %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?location_id={{ object.id }}&show_cables=on&show_unconnected=on" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||
<a role="button" class="btn btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?location_id={{ object.id }}&show_cables=True&show_unconnected=True" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
Topology
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Feed</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Panel</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Power Panel Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Power Panel Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Power Panel Coordinates{% endblock title %}
|
||||
@@ -1,4 +1,4 @@
|
||||
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=on&show_unconnected=on" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||
<a role="button" class="btn btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=True&show_unconnected=True" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
Topology
|
||||
</a>
|
||||
|
||||
@@ -20,6 +20,33 @@ urlpatterns = (
|
||||
path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroup_delete"),
|
||||
path("coordinate-groups/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinategroup_changelog", kwargs={'model': models.CoordinateGroup}),
|
||||
|
||||
# Circuit Coordinate
|
||||
path("circuitcoordinate/", views.CircuitCoordinateListView.as_view(), name="circuitcoordinate_list"),
|
||||
path("circuitcoordinate/add/", views.CircuitCoordinateAddView.as_view(), name="circuitcoordinate_add"),
|
||||
path('circuitcoordinate/import/', views.CircuitCoordinateBulkImportView.as_view(), name='circuitcoordinate_import'),
|
||||
path("circuitcoordinate/<int:pk>/", views.CircuitCoordinateView.as_view(), name="circuitcoordinate"),
|
||||
path("circuitcoordinate/<int:pk>/edit/", views.CircuitCoordinateEditView.as_view(), name="circuitcoordinate_edit"),
|
||||
path("circuitcoordinate/<int:pk>/delete/", views.CircuitCoordinateDeleteView.as_view(), name="circuitcoordinate_delete"),
|
||||
path("circuitcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="circuitcoordinate_changelog", kwargs={'model': models.CircuitCoordinate}),
|
||||
|
||||
# Power Panel Coordinate
|
||||
path("powerpanelcoordinate/", views.PowerPanelCoordinateListView.as_view(), name="powerpanelcoordinate_list"),
|
||||
path("powerpanelcoordinate/add/", views.PowerPanelCoordinateAddView.as_view(), name="powerpanelcoordinate_add"),
|
||||
path('powerpanelcoordinate/import/', views.PowerPanelCoordinateBulkImportView.as_view(), name='powerpanelcoordinate_import'),
|
||||
path("powerpanelcoordinate/<int:pk>/", views.PowerPanelCoordinateView.as_view(), name="powerpanelcoordinate"),
|
||||
path("powerpanelcoordinate/<int:pk>/edit/", views.PowerPanelCoordinateEditView.as_view(), name="powerpanelcoordinate_edit"),
|
||||
path("powerpanelcoordinate/<int:pk>/delete/", views.PowerPanelCoordinateDeleteView.as_view(), name="powerpanelcoordinate_delete"),
|
||||
path("powerpanelcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerpanelcoordinate_changelog", kwargs={'model': models.PowerPanelCoordinate}),
|
||||
|
||||
# Power Feed Coordinate
|
||||
path("powerfeedcoordinate/", views.PowerFeedCoordinateListView.as_view(), name="powerfeedcoordinate_list"),
|
||||
path("powerfeedcoordinate/add/", views.PowerFeedCoordinateAddView.as_view(), name="powerfeedcoordinate_add"),
|
||||
path('powerfeedcoordinate/import/', views.PowerFeedCoordinateBulkImportView.as_view(), name='powerfeedcoordinate_import'),
|
||||
path("powerfeedcoordinate/<int:pk>/", views.PowerFeedCoordinateView.as_view(), name="powerfeedcoordinate"),
|
||||
path("powerfeedcoordinate/<int:pk>/edit/", views.PowerFeedCoordinateEditView.as_view(), name="powerfeedcoordinate_edit"),
|
||||
path("powerfeedcoordinate/<int:pk>/delete/", views.PowerFeedCoordinateDeleteView.as_view(), name="powerfeedcoordinate_delete"),
|
||||
path("powerfeedcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerfeedcoordinate_changelog", kwargs={'model': models.PowerFeedCoordinate}),
|
||||
|
||||
# Coordinate
|
||||
path("coordinate/", views.CoordinateListView.as_view(), name="coordinate_list"),
|
||||
path("coordinate/add/", views.CoordinateAddView.as_view(), name="coordinate_add"),
|
||||
|
||||
@@ -107,9 +107,13 @@ def get_model_role(model: Type[Model]) -> Role:
|
||||
)
|
||||
|
||||
def get_query_settings(request):
|
||||
filter_id = ''
|
||||
if "filter_id" in request.GET:
|
||||
filter_id = request.GET["filter_id"]
|
||||
|
||||
save_coords = False
|
||||
if "save_coords" in request.GET:
|
||||
if request.GET["save_coords"] == "on":
|
||||
if request.GET["save_coords"] == "True":
|
||||
save_coords = True
|
||||
# General options overrides
|
||||
if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False:
|
||||
@@ -119,47 +123,88 @@ def get_query_settings(request):
|
||||
save_coords = True
|
||||
|
||||
# Individual options
|
||||
ignore_cable_type = ()
|
||||
if "ignore_cable_type" in request.GET:
|
||||
ignore_cable_type = request.GET.getlist('ignore_cable_type')
|
||||
|
||||
show_unconnected = False
|
||||
if "show_unconnected" in request.GET:
|
||||
if request.GET["show_unconnected"] == "on":
|
||||
if request.GET["show_unconnected"] == "True":
|
||||
show_unconnected = True
|
||||
|
||||
show_power = False
|
||||
if "show_power" in request.GET:
|
||||
if request.GET["show_power"] == "on":
|
||||
if request.GET["show_power"] == "True":
|
||||
show_power = True
|
||||
|
||||
show_circuit = False
|
||||
if "show_circuit" in request.GET:
|
||||
if request.GET["show_circuit"] == "on":
|
||||
if request.GET["show_circuit"] == "True":
|
||||
show_circuit = True
|
||||
|
||||
show_logical_connections = False
|
||||
if "show_logical_connections" in request.GET:
|
||||
if request.GET["show_logical_connections"] == "on" :
|
||||
if request.GET["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" :
|
||||
if request.GET["show_single_cable_logical_conns"] == "True" :
|
||||
show_single_cable_logical_conns = True
|
||||
|
||||
show_cables = False
|
||||
if "show_cables" in request.GET:
|
||||
if request.GET["show_cables"] == "on" :
|
||||
if request.GET["show_cables"] == "True" :
|
||||
show_cables = True
|
||||
|
||||
show_wireless = False
|
||||
if "show_wireless" in request.GET:
|
||||
if request.GET["show_wireless"] == "on" :
|
||||
if request.GET["show_wireless"] == "True" :
|
||||
show_wireless = True
|
||||
|
||||
group_sites = False
|
||||
if "group_sites" in request.GET:
|
||||
if request.GET["group_sites"] == "True" :
|
||||
group_sites = True
|
||||
|
||||
group_locations = False
|
||||
if "group_locations" in request.GET:
|
||||
if request.GET["group_locations"] == "True" :
|
||||
group_locations = True
|
||||
|
||||
group_racks = False
|
||||
if "group_racks" in request.GET:
|
||||
if request.GET["group_racks"] == "True" :
|
||||
group_racks = True
|
||||
|
||||
group_virtualchassis = False
|
||||
if "group_virtualchassis" in request.GET:
|
||||
if request.GET["group_virtualchassis"] == "True" :
|
||||
group_virtualchassis = True
|
||||
|
||||
group = False
|
||||
if "group" in request.GET:
|
||||
group = request.GET["group"]
|
||||
|
||||
show_neighbors = False
|
||||
if "show_neighbors" in request.GET:
|
||||
if request.GET["show_neighbors"] == "on" :
|
||||
if request.GET["show_neighbors"] == "True" :
|
||||
show_neighbors = True
|
||||
|
||||
return save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors
|
||||
|
||||
straight_cables = False
|
||||
if "straight_cables" in request.GET:
|
||||
if request.GET["straight_cables"] == "True":
|
||||
straight_cables = True
|
||||
|
||||
grid_size = 0
|
||||
if "grid_size" in request.GET:
|
||||
grid_size = request.GET.getlist('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]
|
||||
|
||||
+336
-58
@@ -4,8 +4,7 @@ from typing import DefaultDict, Dict, Optional, Union
|
||||
import time
|
||||
from itertools import chain
|
||||
|
||||
from utilities.htmx import is_htmx
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
||||
from dcim.models import (
|
||||
Cable,
|
||||
CableTermination,
|
||||
@@ -27,8 +26,9 @@ from django.db.models.functions import Lower
|
||||
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import View
|
||||
from extras.models import Tag
|
||||
from extras.models import Tag, SavedFilter
|
||||
from wireless.models import WirelessLink
|
||||
from utilities.htmx import htmx_partial
|
||||
from netbox.views.generic import (
|
||||
ObjectView,
|
||||
ObjectListView,
|
||||
@@ -37,20 +37,36 @@ from netbox.views.generic import (
|
||||
ObjectChangeLogView,
|
||||
BulkImportView
|
||||
)
|
||||
|
||||
|
||||
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet
|
||||
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet, CircuitCoordinatesFilterSet, PowerPanelCoordinatesFilterSet, PowerFeedCoordinatesFilterSet
|
||||
from netbox_topology_views.forms import (
|
||||
DeviceFilterForm,
|
||||
IndividualOptionsForm,
|
||||
CoordinateGroupsForm,
|
||||
CircuitCoordinatesForm,
|
||||
CircuitCoordinatesFilterForm,
|
||||
CircuitCoordinatesImportForm,
|
||||
PowerPanelCoordinatesForm,
|
||||
PowerPanelCoordinatesFilterForm,
|
||||
PowerPanelCoordinatesImportForm,
|
||||
PowerFeedCoordinatesForm,
|
||||
PowerFeedCoordinatesFilterForm,
|
||||
PowerFeedCoordinatesImportForm,
|
||||
CoordinatesForm,
|
||||
CoordinatesFilterForm,
|
||||
CoordinateGroupsImportForm,
|
||||
CoordinatesImportForm
|
||||
)
|
||||
from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions
|
||||
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable
|
||||
import netbox_topology_views.models
|
||||
from netbox_topology_views.models import (
|
||||
RoleImage,
|
||||
IndividualOptions,
|
||||
CoordinateGroup,
|
||||
Coordinate,
|
||||
CircuitCoordinate,
|
||||
PowerPanelCoordinate,
|
||||
PowerFeedCoordinate,
|
||||
)
|
||||
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable, CircuitCoordinateListTable, PowerPanelCoordinateListTable, PowerFeedCoordinateListTable
|
||||
from netbox_topology_views.utils import (
|
||||
CONF_IMAGE_DIR,
|
||||
find_image_url,
|
||||
@@ -62,12 +78,12 @@ 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)
|
||||
query = (
|
||||
{"object_id": entity.device_role_id}
|
||||
{"object_id": entity.role_id}
|
||||
if is_device
|
||||
else {"content_type_id": ContentType.objects.get_for_model(entity).pk}
|
||||
)
|
||||
@@ -76,18 +92,22 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
||||
return RoleImage.objects.get(**query).get_image_url()
|
||||
except RoleImage.DoesNotExist:
|
||||
return find_image_url(
|
||||
entity.device_role.slug if is_device else get_model_slug(entity.__class__)
|
||||
entity.role.slug if is_device else get_model_slug(entity.__class__)
|
||||
)
|
||||
|
||||
|
||||
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 = ""
|
||||
if isinstance(device, Circuit):
|
||||
dev_name = device.cid
|
||||
node["id"] = f"c{device.pk}"
|
||||
model_name = 'CircuitCoordinate'
|
||||
|
||||
if device.provider is not None:
|
||||
node_content += (
|
||||
@@ -98,6 +118,7 @@ def create_node(
|
||||
elif isinstance(device, PowerPanel):
|
||||
dev_name = device.name
|
||||
node["id"] = f"p{device.pk}"
|
||||
model_name = 'PowerPanelCoordinate'
|
||||
|
||||
if device.site is not None:
|
||||
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
||||
@@ -108,6 +129,7 @@ def create_node(
|
||||
elif isinstance(device, PowerFeed):
|
||||
dev_name = device.name
|
||||
node["id"] = f"f{device.pk}"
|
||||
model_name = 'PowerFeedCoordinate'
|
||||
|
||||
if device.power_panel is not None:
|
||||
node_content += (
|
||||
@@ -124,17 +146,18 @@ def create_node(
|
||||
if device.voltage is not None:
|
||||
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
|
||||
else:
|
||||
model_name = 'Coordinate'
|
||||
dev_name = device.name
|
||||
if dev_name is None:
|
||||
dev_name = device.device_type.get_full_name
|
||||
dev_name = device.device_type.full_name
|
||||
|
||||
if device.device_type is not None:
|
||||
node_content += (
|
||||
f"<tr><th>Type: </th><td>{device.device_type.model}</td></tr>"
|
||||
)
|
||||
if device.device_role.name is not None:
|
||||
if device.role.name is not None:
|
||||
node_content += (
|
||||
f"<tr><th>Role: </th><td>{device.device_role.name}</td></tr>"
|
||||
f"<tr><th>Role: </th><td>{device.role.name}</td></tr>"
|
||||
)
|
||||
if device.serial != "":
|
||||
node_content += f"<tr><th>Serial: </th><td>{device.serial}</td></tr>"
|
||||
@@ -160,11 +183,26 @@ def create_node(
|
||||
|
||||
node["id"] = device.pk
|
||||
|
||||
if device.device_role.color != "":
|
||||
node["color.border"] = "#" + device.device_role.color
|
||||
if device.site is not None:
|
||||
node["site"] = device.site.name
|
||||
node["site_id"] = device.site_id
|
||||
if device.location is not None:
|
||||
node["location"] = device.location.name
|
||||
node["location_id"] = device.location_id
|
||||
if device.rack is not None:
|
||||
node["rack"] = device.rack.name
|
||||
node["rack_id"] = device.rack_id
|
||||
if device.virtual_chassis is not None:
|
||||
node["virtual_chassis"] = device.virtual_chassis.name
|
||||
node["virtual_chassis_id"] = device.virtual_chassis_id
|
||||
|
||||
if device.role.color != "":
|
||||
node["color.border"] = "#" + device.role.color
|
||||
|
||||
model_class = getattr(netbox_topology_views.models, model_name)
|
||||
|
||||
if group_id is None or group_id == "default":
|
||||
group_id = Coordinate.get_or_create_default_group(group_id)
|
||||
group_id = model_class.get_or_create_default_group(group_id)
|
||||
if not group_id:
|
||||
print('Exception occured while handling default group.')
|
||||
return node
|
||||
@@ -176,10 +214,10 @@ def create_node(
|
||||
# will not be placed correctly by vis-network.
|
||||
node["x"] = 0
|
||||
node["y"] = 0
|
||||
if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'):
|
||||
if model_class.objects.filter(group=group, device=device.pk).values('x') and model_class.objects.filter(group=group, device=device.pk).values('y'):
|
||||
# Coordinates data for the device exists in Coordinates Group. Let's assign them
|
||||
node["x"] = Coordinate.objects.get(group=group, device=device.pk).x
|
||||
node["y"] = Coordinate.objects.get(group=group, device=device.pk).y
|
||||
node["x"] = model_class.objects.get(group=group, device=device.pk).x
|
||||
node["y"] = model_class.objects.get(group=group, device=device.pk).y
|
||||
node["physics"] = False
|
||||
elif "coordinates" in device.custom_field_data:
|
||||
# We prefer the new Coordinate model but leave the deprecated method
|
||||
@@ -194,10 +232,26 @@ def create_node(
|
||||
dev_title = "<table><tbody> %s</tbody></table>" % (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: getattr(device, 'primary_ip4', None),
|
||||
NodeLabelItems.PRIMARY_IPV6: getattr(device, 'primary_ip6', None),
|
||||
NodeLabelItems.OUT_OF_BAND_IP: getattr(device, 'oob_ip', None),
|
||||
}
|
||||
|
||||
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)
|
||||
node["label_num_lines"] = len(label_items)
|
||||
|
||||
return node
|
||||
|
||||
@@ -206,6 +260,7 @@ def create_edge(
|
||||
edge_id: int,
|
||||
termination_a: Dict,
|
||||
termination_b: Dict,
|
||||
straight_cables: bool,
|
||||
circuit: Optional[Dict] = None,
|
||||
cable: Optional[Cable] = None,
|
||||
wireless: Optional[Dict] = None,
|
||||
@@ -268,6 +323,9 @@ def create_edge(
|
||||
if hasattr(cable, 'color') and cable.color != "":
|
||||
edge["color"] = "#" + cable.color
|
||||
|
||||
# if straight_cables == True: edge["smooth"] = False
|
||||
edge["smooth"] = not straight_cables
|
||||
|
||||
return edge
|
||||
|
||||
|
||||
@@ -295,6 +353,7 @@ def get_topology_data(
|
||||
queryset: QuerySet,
|
||||
individualOptions: IndividualOptions,
|
||||
show_unconnected: bool,
|
||||
ignore_cable_type: list,
|
||||
save_coords: bool,
|
||||
show_cables: bool,
|
||||
show_circuit: bool,
|
||||
@@ -303,7 +362,14 @@ def get_topology_data(
|
||||
show_neighbors: bool,
|
||||
show_power: bool,
|
||||
show_wireless: bool,
|
||||
group_sites: bool,
|
||||
group_locations: bool,
|
||||
group_racks: bool,
|
||||
group_virtualchassis: bool,
|
||||
group_id,
|
||||
straight_cables: bool,
|
||||
grid_size: list,
|
||||
node_label_items: list,
|
||||
):
|
||||
|
||||
supported_termination_types = []
|
||||
@@ -316,6 +382,7 @@ def get_topology_data(
|
||||
nodes_devices = {}
|
||||
edges = []
|
||||
nodes = []
|
||||
options = {}
|
||||
edge_ids = 0
|
||||
nodes_circuits: Dict[int, Circuit] = {}
|
||||
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
||||
@@ -324,8 +391,6 @@ def get_topology_data(
|
||||
cable_ids = DefaultDict(dict)
|
||||
interface_ids = DefaultDict(dict)
|
||||
|
||||
ignore_cable_type = individualOptions.ignore_cable_type
|
||||
|
||||
device_ids = [d.pk for d in queryset]
|
||||
site_ids = [d.site_id for d in queryset]
|
||||
|
||||
@@ -352,12 +417,13 @@ def get_topology_data(
|
||||
)
|
||||
for path_complete_interface in path_complete_interfaces:
|
||||
for connected_endpoint in path_complete_interface.connected_endpoints:
|
||||
device_ids.append(connected_endpoint.device.id)
|
||||
if type(connected_endpoint) != ProviderNetwork:
|
||||
device_ids.append(connected_endpoint.device.id)
|
||||
|
||||
if show_circuit:
|
||||
circuit_terminations = CircuitTermination.objects.filter(
|
||||
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
|
||||
).prefetch_related("provider_network", "circuit")
|
||||
Q(_site_id__in=site_ids) | Q(_provider_network__isnull=False)
|
||||
)
|
||||
for circuit_termination in circuit_terminations:
|
||||
circuit_termination: CircuitTermination
|
||||
if (
|
||||
@@ -378,14 +444,14 @@ def get_topology_data(
|
||||
termination_b = create_circuit_termination(
|
||||
circuit_termination.cable.b_terminations[0]
|
||||
)
|
||||
elif circuit_termination.provider_network is not None:
|
||||
elif circuit_termination.termination is not None:
|
||||
if (
|
||||
circuit_termination.provider_network_id
|
||||
circuit_termination.termination_id
|
||||
not in nodes_provider_networks
|
||||
):
|
||||
nodes_provider_networks[
|
||||
circuit_termination.provider_network.pk
|
||||
] = circuit_termination.provider_network
|
||||
circuit_termination.termination.pk
|
||||
] = circuit_termination.termination
|
||||
|
||||
if bool(termination_a) and bool(termination_b):
|
||||
circuit_model = {
|
||||
@@ -399,6 +465,7 @@ def get_topology_data(
|
||||
circuit=circuit_model,
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -425,7 +492,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, node_label_items, group_id))
|
||||
|
||||
if show_power:
|
||||
power_panels_ids = PowerPanel.objects.filter(
|
||||
@@ -468,6 +535,7 @@ def get_topology_data(
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
power=True,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -475,10 +543,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, node_label_items ,group_id))
|
||||
|
||||
for d in nodes_powerpanel.values():
|
||||
nodes.append(create_node(d, save_coords, group_id))
|
||||
nodes.append(create_node(d, save_coords, node_label_items, group_id))
|
||||
|
||||
if show_logical_connections:
|
||||
interfaces = Interface.objects.filter(
|
||||
@@ -506,7 +574,7 @@ 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, straight_cables=straight_cables))
|
||||
nodes_devices[interface.device.id] = interface.device
|
||||
nodes_devices[destination.device.id] = destination.device
|
||||
|
||||
@@ -516,11 +584,11 @@ def get_topology_data(
|
||||
).select_related("termination_type")
|
||||
|
||||
for link in links:
|
||||
if link.termination_type.name in ignore_cable_type:
|
||||
if link.termination_type.model in ignore_cable_type:
|
||||
continue
|
||||
|
||||
# Normal device cables
|
||||
if link.termination_type.name in supported_termination_types:
|
||||
if link.termination_type.model in supported_termination_types:
|
||||
complete_link = False
|
||||
if link.cable_end == "A":
|
||||
if link.cable_id not in cable_ids:
|
||||
@@ -586,6 +654,7 @@ def get_topology_data(
|
||||
cable=link.cable,
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -625,9 +694,23 @@ def get_topology_data(
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
wireless=wireless,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
if group_locations:
|
||||
options['group_locations'] = 'on'
|
||||
if group_racks:
|
||||
options['group_racks'] = 'on'
|
||||
if group_sites:
|
||||
options['group_sites'] = 'on'
|
||||
if group_virtualchassis:
|
||||
options['group_virtualchassis'] = 'on'
|
||||
if grid_size:
|
||||
options['grid_size'] = grid_size
|
||||
else:
|
||||
options['grid_size'] = list('0')
|
||||
|
||||
for qs_device in queryset:
|
||||
if qs_device.pk not in nodes_devices and show_unconnected:
|
||||
nodes_devices[qs_device.pk] = qs_device
|
||||
@@ -635,11 +718,12 @@ 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
|
||||
results["group"] = group_id
|
||||
results["options"] = options
|
||||
return results
|
||||
|
||||
|
||||
@@ -652,8 +736,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
|
||||
def get(self, request):
|
||||
self.filterset = DeviceFilterSet
|
||||
self.queryset = Device.objects.all().select_related(
|
||||
"device_type", "device_role"
|
||||
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
|
||||
"device_type", "role"
|
||||
)
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
self.model = self.queryset.model
|
||||
@@ -665,10 +749,41 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
|
||||
if request.GET:
|
||||
|
||||
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = 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"] != '':
|
||||
try:
|
||||
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||
saved_filter_params = getattr(saved_filter, 'parameters')
|
||||
|
||||
if ignore_cable_type == () and 'ignore_cable_type' in saved_filter_params: ignore_cable_type = saved_filter_params['ignore_cable_type']
|
||||
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
|
||||
if show_unconnected == False and 'show_unconnected' in saved_filter_params: show_unconnected = saved_filter_params['show_unconnected']
|
||||
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
|
||||
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
|
||||
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
|
||||
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
|
||||
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
|
||||
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
||||
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 group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
|
||||
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:
|
||||
print(type(inst))
|
||||
|
||||
if "group" not in request.GET:
|
||||
group_id = "default"
|
||||
if 'saved_filter_params' in locals() and "group" in saved_filter_params:
|
||||
group_id = saved_filter_params['group'][0]
|
||||
else:
|
||||
group_id = "default"
|
||||
else:
|
||||
group_id = request.GET["group"]
|
||||
|
||||
@@ -676,6 +791,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
topo_data = get_topology_data(
|
||||
queryset=self.queryset,
|
||||
individualOptions=individualOptions,
|
||||
ignore_cable_type=ignore_cable_type,
|
||||
save_coords=save_coords,
|
||||
show_unconnected=show_unconnected,
|
||||
show_cables=show_cables,
|
||||
@@ -685,37 +801,57 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
show_circuit=show_circuit,
|
||||
show_power=show_power,
|
||||
show_wireless=show_wireless,
|
||||
group_sites=group_sites,
|
||||
group_locations=group_locations,
|
||||
group_racks=group_racks,
|
||||
group_virtualchassis=group_virtualchassis,
|
||||
group_id=group_id,
|
||||
straight_cables=straight_cables,
|
||||
grid_size=grid_size,
|
||||
node_label_items=node_label_items,
|
||||
)
|
||||
|
||||
else:
|
||||
# No GET-Request in URL. We most likely came here from the navigation menu.
|
||||
preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
|
||||
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
|
||||
ignore_cable_type = IndividualOptions.objects.get(id=individualOptions.id).ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')
|
||||
if ignore_cable_type == ['']: ignore_cable_type = []
|
||||
|
||||
q = QueryDict(mutable=True)
|
||||
q.setlist("device_role_id", list(preselected_device_roles))
|
||||
q.setlist("role_id", list(preselected_device_roles))
|
||||
q.setlist("tag", list(preselected_tags))
|
||||
q.setlist("ignore_cable_type", ignore_cable_type)
|
||||
|
||||
if individualOptions.save_coords: q['save_coords'] = "True"
|
||||
if individualOptions.show_unconnected: q['show_unconnected'] = "True"
|
||||
if individualOptions.show_cables: q['show_cables'] = "True"
|
||||
if individualOptions.show_logical_connections: q['show_logical_connections'] = "True"
|
||||
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "True"
|
||||
if individualOptions.show_neighbors: q['show_neighbors'] = "True"
|
||||
if individualOptions.show_circuit: q['show_circuit'] = "True"
|
||||
if individualOptions.show_power: q['show_power'] = "True"
|
||||
if individualOptions.show_wireless: q['show_wireless'] = "True"
|
||||
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.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.save_coords: q['save_coords'] = "on"
|
||||
if individualOptions.show_unconnected: q['show_unconnected'] = "on"
|
||||
if individualOptions.show_cables: q['show_cables'] = "on"
|
||||
if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
|
||||
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
|
||||
if individualOptions.show_neighbors: q['show_neighbors'] = "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"
|
||||
q['draw_init'] = "True"
|
||||
else:
|
||||
q['draw_init'] = "false"
|
||||
q['draw_init'] = "False"
|
||||
|
||||
query_string = q.urlencode()
|
||||
return HttpResponseRedirect(f"{request.path}?{query_string}")
|
||||
|
||||
|
||||
if is_htmx(request):
|
||||
if htmx_partial(request):
|
||||
return render(
|
||||
request,
|
||||
"netbox_topology_views/htmx_topology.html",
|
||||
@@ -748,9 +884,9 @@ ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
||||
class TopologyImagesView(PermissionRequiredMixin, View):
|
||||
permission_required = (
|
||||
"dcim.view_site",
|
||||
"dcim.view_device_role",
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
"dcim.view_devicerole",
|
||||
"dcim.add_devicerole",
|
||||
"dcim.change_devicerole",
|
||||
)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
@@ -799,6 +935,132 @@ class TopologyImagesView(PermissionRequiredMixin, View):
|
||||
},
|
||||
)
|
||||
|
||||
class CircuitCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class CircuitCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
form = CircuitCoordinatesForm
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_add.html'
|
||||
|
||||
class CircuitCoordinateBulkImportView(BulkImportView):
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
model_form = CircuitCoordinatesImportForm
|
||||
|
||||
class CircuitCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
table = CircuitCoordinateListTable
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_list.html'
|
||||
filterset = CircuitCoordinatesFilterSet
|
||||
filterset_form = CircuitCoordinatesFilterForm
|
||||
|
||||
class CircuitCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
form = CircuitCoordinatesForm
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_edit.html'
|
||||
|
||||
class CircuitCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class CircuitCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
form = PowerPanelCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_add.html'
|
||||
|
||||
class PowerPanelCoordinateBulkImportView(BulkImportView):
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
model_form = PowerPanelCoordinatesImportForm
|
||||
|
||||
class PowerPanelCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
table = PowerPanelCoordinateListTable
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_list.html'
|
||||
filterset = PowerPanelCoordinatesFilterSet
|
||||
filterset_form = PowerPanelCoordinatesFilterForm
|
||||
|
||||
class PowerPanelCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
form = PowerPanelCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_edit.html'
|
||||
|
||||
class PowerPanelCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
form = PowerFeedCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_add.html'
|
||||
|
||||
class PowerFeedCoordinateBulkImportView(BulkImportView):
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
model_form = PowerFeedCoordinatesImportForm
|
||||
|
||||
class PowerFeedCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
table = PowerFeedCoordinateListTable
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_list.html'
|
||||
filterset = PowerFeedCoordinatesFilterSet
|
||||
filterset_form = PowerFeedCoordinatesFilterForm
|
||||
|
||||
class PowerFeedCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
form = PowerFeedCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_edit.html'
|
||||
|
||||
class PowerFeedCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class CoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
@@ -847,10 +1109,19 @@ class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
|
||||
queryset = CoordinateGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuittable = CircuitCoordinateListTable(instance.circuitcoordinate_set.all())
|
||||
circuittable.configure(request)
|
||||
powerpaneltable = PowerPanelCoordinateListTable(instance.powerpanelcoordinate_set.all())
|
||||
powerpaneltable.configure(request)
|
||||
powerfeedtable = PowerFeedCoordinateListTable(instance.powerfeedcoordinate_set.all())
|
||||
powerfeedtable.configure(request)
|
||||
table = CoordinateListTable(instance.coordinate_set.all())
|
||||
table.configure(request)
|
||||
|
||||
return {
|
||||
'circuitcoordinates_table': circuittable,
|
||||
'powerpanelcoordinates_table': powerpaneltable,
|
||||
'powerfeedcoordinates_table': powerfeedtable,
|
||||
'coordinates_table': table,
|
||||
}
|
||||
|
||||
@@ -925,7 +1196,14 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
||||
'show_circuit': queryset.show_circuit,
|
||||
'show_power': queryset.show_power,
|
||||
'show_wireless': queryset.show_wireless,
|
||||
'group_sites': queryset.group_sites,
|
||||
'group_locations': queryset.group_locations,
|
||||
'group_racks': queryset.group_racks,
|
||||
'group_virtualchassis': queryset.group_virtualchassis,
|
||||
'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(', ')),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user