Compare commits
29
Commits
v4.0.0
...
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 |
@@ -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
|
## 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.
|
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.
|
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
|
||||||
|
|
||||||
|
Then run:
|
||||||
Then run
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/netbox/netbox
|
cd /opt/netbox/netbox
|
||||||
pip3 install netbox-topology-views
|
pip3 install netbox-topology-views
|
||||||
@@ -46,22 +30,40 @@ python3 manage.py migrate netbox_topology_views
|
|||||||
python3 manage.py collectstatic --no-input
|
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
|
### 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 |
|
| netbox version | netbox-topology-views version |
|
||||||
| -------------- | ----------------------------- |
|
| -------------- | ----------------------------- |
|
||||||
| >= 4.0.0 | >= v4.0.0 |
|
| = 4.3.X | = v4.2.1 |
|
||||||
| >= 3.7.0 | >= v3.9.0 |
|
| = 4.2.1 | = v4.2.1 |
|
||||||
| >= 3.6.4 | >= v3.8.1 |
|
| = 4.2.0 | = v4.2.0 |
|
||||||
| >= 3.6.0 | >= v3.7.X |
|
| = 4.1.X | = v4.1.0 |
|
||||||
| >= 3.5.0 | >= v3.4.X |
|
| = 4.0.X | = v4.0.0 |
|
||||||
| >= 3.4.0 | >= v3.X.X |
|
| = 3.7.0 | = v3.9.0 |
|
||||||
| >= 3.3.0 | >= v3.0.0 |
|
| = 3.6.4 | = v3.8.1 |
|
||||||
| >= 3.2.0 | >= v1.1.0 |
|
| = 3.6.0 | = v3.7.0 |
|
||||||
| >= 3.1.8 | >= v1.0.0 |
|
| = 3.5.0 | = v3.6.2 |
|
||||||
| >= 2.11.1 | >= v0.5.3 |
|
|
||||||
| >= 2.10.0 | >= v0.5.0 |
|
|
||||||
| < 2.10.0 | =< v0.4.10 |
|
|
||||||
|
|
||||||
### Update
|
### Update
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
version: '3.4'
|
version: '3.4'
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
|
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
|
||||||
POSTGRES_DB: netbox
|
POSTGRES_DB: netbox
|
||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- netbox-postgres-data:/var/lib/postgresql/data
|
- netbox-postgres-data-16:/var/lib/postgresql/data
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6-alpine
|
image: redis:6-alpine
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "6378:6379"
|
- "6378:6379"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
netbox-postgres-data:
|
netbox-postgres-data-16:
|
||||||
driver: local
|
driver: local
|
||||||
netbox-redis-data:
|
netbox-redis-data:
|
||||||
driver: local
|
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
|
||||||
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
|
|||||||
name = "netbox_topology_views"
|
name = "netbox_topology_views"
|
||||||
verbose_name = "Topology views"
|
verbose_name = "Topology views"
|
||||||
description = "An plugin to render topology maps"
|
description = "An plugin to render topology maps"
|
||||||
version = "4.0.0"
|
version = "4.2.1"
|
||||||
author = "Mattijs Vanhaverbeke"
|
author = "Mattijs Vanhaverbeke"
|
||||||
author_email = "author@example.com"
|
author_email = "author@example.com"
|
||||||
base_url = "netbox_topology_views"
|
base_url = "netbox_topology_views"
|
||||||
|
|||||||
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
|
|||||||
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IndividualOptions
|
model = IndividualOptions
|
||||||
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout")
|
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size", "node_label_items")
|
||||||
|
|||||||
@@ -7,5 +7,10 @@ router = NetBoxRouter()
|
|||||||
router.register("save-coords", views.SaveCoordsViewSet)
|
router.register("save-coords", views.SaveCoordsViewSet)
|
||||||
router.register("images", views.SaveRoleImageViewSet)
|
router.register("images", views.SaveRoleImageViewSet)
|
||||||
router.register("xml-export", views.ExportTopoToXML, "xml-export")
|
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
|
urlpatterns = router.urls
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||||
from extras.models import SavedFilter
|
from extras.models import SavedFilter
|
||||||
@@ -15,6 +17,11 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
|||||||
from netbox_topology_views.api.serializers import (
|
from netbox_topology_views.api.serializers import (
|
||||||
RoleImageSerializer,
|
RoleImageSerializer,
|
||||||
TopologyDummySerializer,
|
TopologyDummySerializer,
|
||||||
|
CoordinateGroupSerializer,
|
||||||
|
CoordinateSerializer,
|
||||||
|
CircuitCoordinateSerializer,
|
||||||
|
PowerPanelCoordinateSerializer,
|
||||||
|
PowerFeedCoordinateSerializer,
|
||||||
)
|
)
|
||||||
import netbox_topology_views.models
|
import netbox_topology_views.models
|
||||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||||
@@ -22,7 +29,7 @@ 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.utils import get_image_from_url, export_data_to_xml, get_query_settings
|
||||||
from netbox_topology_views.filters import DeviceFilterSet
|
from netbox_topology_views.filters import DeviceFilterSet
|
||||||
|
|
||||||
class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
class SaveCoordsViewSet(BaseViewSet, ReadOnlyModelViewSet):
|
||||||
permission_required = 'netbox_topology_views.change_coordinate'
|
permission_required = 'netbox_topology_views.change_coordinate'
|
||||||
|
|
||||||
queryset = Device.objects.none()
|
queryset = Device.objects.none()
|
||||||
@@ -89,16 +96,14 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
return Response({"status": "saved coords"})
|
return Response({"status": "saved coords"})
|
||||||
|
|
||||||
class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
class ExportTopoToXML(BaseViewSet, ViewSet):
|
||||||
permission_required = ("dcim.view_site", "dcim.view_device")
|
|
||||||
|
|
||||||
queryset = Device.objects.none()
|
queryset = Device.objects.none()
|
||||||
serializer_class = TopologyDummySerializer
|
serializer_class = TopologyDummySerializer
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
self.filterset = DeviceFilterSet
|
self.filterset = DeviceFilterSet
|
||||||
self.queryset = Device.objects.all().select_related(
|
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
|
||||||
"device_type", "role"
|
"device_type", "role"
|
||||||
)
|
)
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
@@ -109,7 +114,7 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
|
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
|
||||||
filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks,show_neighbors = get_query_settings(request)
|
filter_id, 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
|
# Read options from saved filters as NetBox does not handle custom plugin filters
|
||||||
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
||||||
@@ -117,7 +122,9 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||||
saved_filter_params = getattr(saved_filter, 'parameters')
|
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 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_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_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_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||||
@@ -127,19 +134,27 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
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_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_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 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
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
pass
|
pass
|
||||||
except Exception as inst:
|
except Exception as inst:
|
||||||
print(type(inst))
|
print(type(inst))
|
||||||
|
|
||||||
if 'group' not in request.query_params:
|
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:
|
else:
|
||||||
group_id = request.query_params["group"]
|
group_id = request.query_params["group"]
|
||||||
topo_data = get_topology_data(
|
topo_data = get_topology_data(
|
||||||
queryset=self.queryset,
|
queryset=self.queryset,
|
||||||
individualOptions=individualOptions,
|
individualOptions=individualOptions,
|
||||||
|
ignore_cable_type=ignore_cable_type,
|
||||||
save_coords=save_coords,
|
save_coords=save_coords,
|
||||||
show_unconnected=show_unconnected,
|
show_unconnected=show_unconnected,
|
||||||
show_cables=show_cables,
|
show_cables=show_cables,
|
||||||
@@ -152,9 +167,13 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
group_sites=group_sites,
|
group_sites=group_sites,
|
||||||
group_locations=group_locations,
|
group_locations=group_locations,
|
||||||
group_racks=group_racks,
|
group_racks=group_racks,
|
||||||
|
group_virtualchassis=group_virtualchassis,
|
||||||
group_id=group_id,
|
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")
|
return HttpResponse(xml_data, content_type="application/xml; charset=utf-8")
|
||||||
else:
|
else:
|
||||||
@@ -162,7 +181,7 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
{"status": "Missing or malformed request parameters"}, status=400
|
{"status": "Missing or malformed request parameters"}, status=400
|
||||||
)
|
)
|
||||||
|
|
||||||
class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
class SaveRoleImageViewSet(BaseViewSet, ReadOnlyModelViewSet):
|
||||||
queryset = DeviceRole.objects.none()
|
queryset = DeviceRole.objects.none()
|
||||||
serializer_class = RoleImageSerializer
|
serializer_class = RoleImageSerializer
|
||||||
permission_required = (
|
permission_required = (
|
||||||
@@ -240,3 +259,23 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({"status": "Ok"})
|
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'),
|
||||||
|
]
|
||||||
@@ -71,7 +71,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
null_value=None,
|
null_value=None,
|
||||||
)
|
)
|
||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
field_name='interfaces__mac_address',
|
field_name='interfaces__mac_addresses__mac_address',
|
||||||
label="MAC address",
|
label="MAC address",
|
||||||
)
|
)
|
||||||
serial = MultiValueCharFilter(
|
serial = MultiValueCharFilter(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from utilities.forms.fields import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||||
|
from netbox_topology_views.choices import NodeLabelItems
|
||||||
|
|
||||||
class DeviceFilterForm(
|
class DeviceFilterForm(
|
||||||
LocalConfigContextFilterForm,
|
LocalConfigContextFilterForm,
|
||||||
@@ -34,9 +35,9 @@ class DeviceFilterForm(
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
'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',
|
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
|
||||||
'group_sites', 'group_locations', 'group_racks', name=_("Options")
|
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', 'node_label_items', name=_("Options")
|
||||||
),
|
),
|
||||||
FieldSet('id', name=_("Device")),
|
FieldSet('id', name=_("Device")),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
|
||||||
@@ -229,6 +230,11 @@ class DeviceFilterForm(
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
# options
|
# options
|
||||||
|
ignore_cable_type = forms.MultipleChoiceField(
|
||||||
|
label=_('Ignore Termination Types'),
|
||||||
|
required=False,
|
||||||
|
choices=IndividualOptions.CHOICES
|
||||||
|
)
|
||||||
save_coords = forms.NullBooleanField(
|
save_coords = forms.NullBooleanField(
|
||||||
label=_('Save Coordinates'),
|
label=_('Save Coordinates'),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -240,7 +246,7 @@ class DeviceFilterForm(
|
|||||||
)
|
)
|
||||||
show_unconnected = forms.NullBooleanField(
|
show_unconnected = forms.NullBooleanField(
|
||||||
label=_('Show Unconnected'),
|
label=_('Show Unconnected'),
|
||||||
required=False,
|
required=False,
|
||||||
initial=False,
|
initial=False,
|
||||||
widget=forms.Select(
|
widget=forms.Select(
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
@@ -310,6 +316,31 @@ class DeviceFilterForm(
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
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):
|
class CoordinateGroupsForm(NetBoxModelForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -485,7 +516,6 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet
|
FieldSet
|
||||||
(
|
(
|
||||||
'user_id',
|
|
||||||
'ignore_cable_type',
|
'ignore_cable_type',
|
||||||
'preselected_device_roles',
|
'preselected_device_roles',
|
||||||
'preselected_tags',
|
'preselected_tags',
|
||||||
@@ -501,7 +531,11 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
'group_sites',
|
'group_sites',
|
||||||
'group_locations',
|
'group_locations',
|
||||||
'group_racks',
|
'group_racks',
|
||||||
|
'group_virtualchassis',
|
||||||
'draw_default_layout',
|
'draw_default_layout',
|
||||||
|
'straight_cables',
|
||||||
|
'grid_size',
|
||||||
|
'node_label_items',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -622,6 +656,13 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
help_text=_('Draws a rectangle around Devices that belong to the '
|
help_text=_('Draws a rectangle around Devices that belong to the '
|
||||||
'same rack.')
|
'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(
|
draw_default_layout = forms.BooleanField(
|
||||||
label = ('Draw Default Layout'),
|
label = ('Draw Default Layout'),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -629,6 +670,29 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
help_text=_('Enable this option if you want to draw the topology on '
|
help_text=_('Enable this option if you want to draw the topology on '
|
||||||
'the initial load (when you go to the topology plugin page).')
|
'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:
|
class Meta:
|
||||||
model = IndividualOptions
|
model = IndividualOptions
|
||||||
@@ -636,5 +700,6 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
|
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
|
||||||
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
||||||
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
|
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
|
||||||
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'draw_default_layout'
|
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'draw_default_layout',
|
||||||
|
'straight_cables', 'grid_size', 'node_label_items'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -25,6 +25,7 @@ from netbox_topology_views.utils import (
|
|||||||
image_static_url,
|
image_static_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from netbox_topology_views.choices import NodeLabelItems
|
||||||
|
|
||||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -331,12 +332,12 @@ class PowerFeedCoordinate(NetBoxModel):
|
|||||||
class IndividualOptions(NetBoxModel):
|
class IndividualOptions(NetBoxModel):
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
('interface', 'interface'),
|
('interface', 'interface'),
|
||||||
('front port', 'front port'),
|
('frontport', 'frontport'),
|
||||||
('rear port', 'rear port'),
|
('rearport', 'rearport'),
|
||||||
('power outlet', 'power outlet'),
|
('poweroutlet', 'poweroutlet'),
|
||||||
('power port', 'power port'),
|
('powerport', 'powerport'),
|
||||||
('console port', 'console port'),
|
('consoleport', 'consoleport'),
|
||||||
('console server port', 'console server port'),
|
('consoleserverport', 'consoleserverport'),
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = models.IntegerField(
|
user_id = models.IntegerField(
|
||||||
@@ -395,10 +396,23 @@ class IndividualOptions(NetBoxModel):
|
|||||||
group_racks = models.BooleanField(
|
group_racks = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
group_virtualchassis = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
draw_default_layout = models.BooleanField(
|
draw_default_layout = models.BooleanField(
|
||||||
default=False
|
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
|
_netbox_private = True
|
||||||
|
|
||||||
def __str___(self):
|
def __str___(self):
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -73,11 +73,131 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
const group_sites = topologyData.options.group_sites
|
const group_sites = topologyData.options.group_sites
|
||||||
const group_locations = topologyData.options.group_locations
|
const group_locations = topologyData.options.group_locations
|
||||||
const group_racks = topologyData.options.group_racks
|
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 = new Network(container, { nodes, edges }, options)
|
||||||
graph.fit()
|
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) => {
|
graph.on('dragEnd', (params) => {
|
||||||
|
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
|
if (coordSaveCheckbox.options[coordSaveCheckbox.selectedIndex].text != "Yes") return
|
||||||
|
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
@@ -138,21 +258,30 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
graph.on('beforeDrawing', (canvascontext) => {
|
||||||
|
if (gridSize > 0) {
|
||||||
|
drawGrid(canvascontext);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
graph.on('afterDrawing', (canvascontext) => {
|
graph.on('afterDrawing', (canvascontext) => {
|
||||||
allRectangles = [];
|
allRectangles = [];
|
||||||
if(group_sites != null && group_sites == 'on') { drawGroupRectangles(canvascontext, groupedNodeSites, siteRectParams); }
|
if(group_sites != null && group_sites == 'on') { drawGroupRectangles(canvascontext, groupedNodeSites, siteRectParams); }
|
||||||
if(group_locations != null && group_locations == 'on') { drawGroupRectangles(canvascontext, groupedNodeLocations, locationRectParams); }
|
if(group_locations != null && group_locations == 'on') { drawGroupRectangles(canvascontext, groupedNodeLocations, locationRectParams); }
|
||||||
if(group_racks != null && group_racks == 'on') { drawGroupRectangles(canvascontext, groupedNodeRacks, rackRectParams); }
|
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) => {
|
graph.on('click', (canvascontext) => {
|
||||||
allRectangles.forEach(key => {
|
allRectangles.forEach(key => {
|
||||||
// Is the mouse pointer inside of the current rectangle?
|
// Is the mouse pointer inside of the current rectangle?
|
||||||
if(canvascontext.pointer.canvas.x > (key.x1 - key.border / 2 - 1) && canvascontext.pointer.canvas.x < (key.x2 + key.border / 2 + 1)
|
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 - 1) && canvascontext.pointer.canvas.y < (key.y2 + key.border / 2 + 1)) {
|
&& 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
|
// 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 + 1) || canvascontext.pointer.canvas.x > (key.x2 - key.border / 2 - 1)
|
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 + 1) || canvascontext.pointer.canvas.y > (key.y2 - key.border / 2 - 1)) {
|
|| 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
|
// Generate an array of affected nodes in order to pass it to the select.Nodes() function
|
||||||
let arr = [];
|
let arr = [];
|
||||||
if(key.category == "Site") {
|
if(key.category == "Site") {
|
||||||
@@ -182,20 +311,29 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if(key.category == "Virtual Chassis") {
|
||||||
|
groupedNodeVirtualchassis.forEach(subArray => {
|
||||||
|
subArray.forEach(element => {
|
||||||
|
if (element[1] === key.id) {
|
||||||
|
arr.push(element[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
graph.selectNodes(arr);
|
graph.selectNodes(arr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add information on which node belongs to which group (site/location/rack).
|
// 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
|
// Create an array for each group in order to loop through that arrays later
|
||||||
function combineNodeInfo(typeId, type) {
|
function combineNodeInfo(typeId, type) {
|
||||||
let nodesArray = [];
|
let nodesArray = [];
|
||||||
// Extract node ids and node type ids from all nodes
|
// Extract node ids and node type ids from all nodes
|
||||||
for (let [key, value] of nodes._data) {
|
for (let [key, value] of nodes._data) {
|
||||||
if (value[typeId] != undefined) {
|
if (value[typeId] != undefined) {
|
||||||
nodesArray.push([value.id, value[typeId], value[type]]);
|
nodesArray.push([value.id, value[typeId], value[type], value.label_num_lines]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Split single array above into arrays grouped by node id
|
// Split single array above into arrays grouped by node id
|
||||||
@@ -238,7 +376,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
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})
|
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/rack)
|
/* Draw all rectangles of a given group (site/location//virtualchassis)
|
||||||
rectParams expects an object that consists of the following keys:
|
rectParams expects an object that consists of the following keys:
|
||||||
lineWidth: border width (string)
|
lineWidth: border width (string)
|
||||||
color: border color (string)
|
color: border color (string)
|
||||||
@@ -252,16 +390,24 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
const rectangles = [];
|
const rectangles = [];
|
||||||
const xValues = [];
|
const xValues = [];
|
||||||
const yValues = [];
|
const yValues = [];
|
||||||
|
const yValuesWithLabel = [];
|
||||||
|
const labelLineHeight = 16;
|
||||||
|
|
||||||
for(let val of value[1]) {
|
// Get coordinates for every node in a given group
|
||||||
|
for(let val of value[1]) {
|
||||||
xValues.push(graph.getPosition(val[0]).x);
|
xValues.push(graph.getPosition(val[0]).x);
|
||||||
yValues.push(graph.getPosition(val[0]).y);
|
yValues.push(graph.getPosition(val[0]).y);
|
||||||
|
yValuesWithLabel.push(yValues[yValues.length -1] + val[3] * labelLineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rectX = Math.min(...xValues) - rectParams.paddingX;
|
const minX = Math.min(...xValues);
|
||||||
const rectY = Math.min(...yValues) - rectParams.paddingY;
|
const maxX = Math.max(...xValues);
|
||||||
const rectSizeX = Math.max(...xValues) - Math.min(...xValues) + 2*rectParams.paddingX;
|
const minY = Math.min(...yValues);
|
||||||
const rectSizeY = Math.max(...yValues) - Math.min(...yValues) + 2*rectParams.paddingY;
|
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({
|
rectangles.push({
|
||||||
ctx: canvascontext,
|
ctx: canvascontext,
|
||||||
@@ -303,8 +449,8 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
color: "#337ab7",
|
color: "#337ab7",
|
||||||
paddingX: 77,
|
paddingX: 77,
|
||||||
paddingY: 77,
|
paddingY: 77,
|
||||||
textPaddingX: 12,
|
textPaddingX: 22,
|
||||||
textPaddingY: 22,
|
textPaddingY: 29,
|
||||||
font: "14px helvetica",
|
font: "14px helvetica",
|
||||||
category: "Location"
|
category: "Location"
|
||||||
}
|
}
|
||||||
@@ -315,11 +461,23 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
color: "green",
|
color: "green",
|
||||||
paddingX: 70,
|
paddingX: 70,
|
||||||
paddingY: 70,
|
paddingY: 70,
|
||||||
textPaddingX: 8,
|
textPaddingX: 15,
|
||||||
textPaddingY: 30,
|
textPaddingY: 36,
|
||||||
font: "14px helvetica",
|
font: "14px helvetica",
|
||||||
category: "Rack"
|
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
|
// Download Graph
|
||||||
@@ -361,12 +519,12 @@ function performXmlDownload() {
|
|||||||
if (curr_url.includes(sites_prefix)) {
|
if (curr_url.includes(sites_prefix)) {
|
||||||
var site_id = curr_url.split(sites_prefix)[1];
|
var site_id = curr_url.split(sites_prefix)[1];
|
||||||
site_id = site_id.split('/')[0]
|
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)) {
|
else if (curr_url.includes(location_prefix)) {
|
||||||
var location_id = curr_url.split(location_prefix)[1];
|
var location_id = curr_url.split(location_prefix)[1];
|
||||||
location_id = location_id.split('/')[0]
|
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 {
|
else {
|
||||||
|
|||||||
+1306
-435
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "4.0.0",
|
"version": "4.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bundle": "node bundle.js",
|
"bundle": "node bundle.js",
|
||||||
"bundle:styles": "node bundle.js --styles",
|
"bundle:styles": "node bundle.js --styles",
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.0",
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
"component-emitter": "^1.3.0",
|
"component-emitter": "^1.3.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.25.0",
|
||||||
"esbuild-sass-plugin": "^1.5.2",
|
"esbuild-sass-plugin": "^3.3.1",
|
||||||
"keycharm": "^0.4.0",
|
"keycharm": "^0.4.0",
|
||||||
"timsort": "^0.3.0",
|
"timsort": "^0.3.0",
|
||||||
"uuid": "^8.0.0"
|
"uuid": "^8.0.0"
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ from netbox.plugins import PluginTemplateExtension
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
|
NETBOX_CURRENT_VERSION = version.parse(settings.RELEASE.version)
|
||||||
|
|
||||||
|
|
||||||
class SiteButtons(PluginTemplateExtension):
|
class SiteButtons(PluginTemplateExtension):
|
||||||
model = 'dcim.site'
|
models = ('dcim.site', )
|
||||||
def buttons(self):
|
def buttons(self):
|
||||||
return self.render('netbox_topology_views/site_button.html')
|
return self.render('netbox_topology_views/site_button.html')
|
||||||
|
|
||||||
class LocationButtons(PluginTemplateExtension):
|
class LocationButtons(PluginTemplateExtension):
|
||||||
model = 'dcim.location'
|
models = ('dcim.location', )
|
||||||
def buttons(self):
|
def buttons(self):
|
||||||
return self.render('netbox_topology_views/location_button.html')
|
return self.render('netbox_topology_views/location_button.html')
|
||||||
|
|
||||||
|
|||||||
@@ -40,5 +40,6 @@
|
|||||||
var is_htmx = true;
|
var is_htmx = true;
|
||||||
var brokenImage = '{{ broken_image }}';
|
var brokenImage = '{{ broken_image }}';
|
||||||
var topologyData = {{ topology_data | safe }};
|
var topologyData = {{ topology_data | safe }};
|
||||||
|
const basePath = '{{ basepath }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
|
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ def get_query_settings(request):
|
|||||||
save_coords = True
|
save_coords = True
|
||||||
|
|
||||||
# Individual options
|
# Individual options
|
||||||
|
ignore_cable_type = ()
|
||||||
|
if "ignore_cable_type" in request.GET:
|
||||||
|
ignore_cable_type = request.GET.getlist('ignore_cable_type')
|
||||||
|
|
||||||
show_unconnected = False
|
show_unconnected = False
|
||||||
if "show_unconnected" in request.GET:
|
if "show_unconnected" in request.GET:
|
||||||
if request.GET["show_unconnected"] == "True":
|
if request.GET["show_unconnected"] == "True":
|
||||||
@@ -173,12 +177,34 @@ def get_query_settings(request):
|
|||||||
if request.GET["group_racks"] == "True" :
|
if request.GET["group_racks"] == "True" :
|
||||||
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
|
show_neighbors = False
|
||||||
if "show_neighbors" in request.GET:
|
if "show_neighbors" in request.GET:
|
||||||
if request.GET["show_neighbors"] == "True" :
|
if request.GET["show_neighbors"] == "True" :
|
||||||
show_neighbors = True
|
show_neighbors = True
|
||||||
|
|
||||||
return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors
|
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():
|
class LinePattern():
|
||||||
wireless = [2, 10, 2, 10]
|
wireless = [2, 10, 2, 10]
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ from netbox_topology_views.utils import (
|
|||||||
IMAGE_FILETYPES
|
IMAGE_FILETYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from netbox_topology_views.choices import NodeLabelItems
|
||||||
|
|
||||||
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
||||||
is_device = isinstance(entity, Device)
|
is_device = isinstance(entity, Device)
|
||||||
@@ -97,7 +97,10 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
|||||||
|
|
||||||
|
|
||||||
def create_node(
|
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 = {}
|
||||||
node_content = ""
|
node_content = ""
|
||||||
@@ -146,7 +149,7 @@ def create_node(
|
|||||||
model_name = 'Coordinate'
|
model_name = 'Coordinate'
|
||||||
dev_name = device.name
|
dev_name = device.name
|
||||||
if dev_name is None:
|
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:
|
if device.device_type is not None:
|
||||||
node_content += (
|
node_content += (
|
||||||
@@ -189,6 +192,9 @@ def create_node(
|
|||||||
if device.rack is not None:
|
if device.rack is not None:
|
||||||
node["rack"] = device.rack.name
|
node["rack"] = device.rack.name
|
||||||
node["rack_id"] = device.rack_id
|
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 != "":
|
if device.role.color != "":
|
||||||
node["color.border"] = "#" + device.role.color
|
node["color.border"] = "#" + device.role.color
|
||||||
@@ -226,10 +232,26 @@ def create_node(
|
|||||||
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
|
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
|
||||||
node["title"] = dev_title
|
node["title"] = dev_title
|
||||||
node["name"] = dev_name
|
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["shape"] = "image"
|
||||||
node["href"] = device.get_absolute_url()
|
node["href"] = device.get_absolute_url()
|
||||||
node["image"] = get_image_for_entity(device)
|
node["image"] = get_image_for_entity(device)
|
||||||
|
node["label_num_lines"] = len(label_items)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
@@ -238,6 +260,7 @@ def create_edge(
|
|||||||
edge_id: int,
|
edge_id: int,
|
||||||
termination_a: Dict,
|
termination_a: Dict,
|
||||||
termination_b: Dict,
|
termination_b: Dict,
|
||||||
|
straight_cables: bool,
|
||||||
circuit: Optional[Dict] = None,
|
circuit: Optional[Dict] = None,
|
||||||
cable: Optional[Cable] = None,
|
cable: Optional[Cable] = None,
|
||||||
wireless: Optional[Dict] = None,
|
wireless: Optional[Dict] = None,
|
||||||
@@ -300,6 +323,9 @@ def create_edge(
|
|||||||
if hasattr(cable, 'color') and cable.color != "":
|
if hasattr(cable, 'color') and cable.color != "":
|
||||||
edge["color"] = "#" + cable.color
|
edge["color"] = "#" + cable.color
|
||||||
|
|
||||||
|
# if straight_cables == True: edge["smooth"] = False
|
||||||
|
edge["smooth"] = not straight_cables
|
||||||
|
|
||||||
return edge
|
return edge
|
||||||
|
|
||||||
|
|
||||||
@@ -327,6 +353,7 @@ def get_topology_data(
|
|||||||
queryset: QuerySet,
|
queryset: QuerySet,
|
||||||
individualOptions: IndividualOptions,
|
individualOptions: IndividualOptions,
|
||||||
show_unconnected: bool,
|
show_unconnected: bool,
|
||||||
|
ignore_cable_type: list,
|
||||||
save_coords: bool,
|
save_coords: bool,
|
||||||
show_cables: bool,
|
show_cables: bool,
|
||||||
show_circuit: bool,
|
show_circuit: bool,
|
||||||
@@ -338,7 +365,11 @@ def get_topology_data(
|
|||||||
group_sites: bool,
|
group_sites: bool,
|
||||||
group_locations: bool,
|
group_locations: bool,
|
||||||
group_racks: bool,
|
group_racks: bool,
|
||||||
|
group_virtualchassis: bool,
|
||||||
group_id,
|
group_id,
|
||||||
|
straight_cables: bool,
|
||||||
|
grid_size: list,
|
||||||
|
node_label_items: list,
|
||||||
):
|
):
|
||||||
|
|
||||||
supported_termination_types = []
|
supported_termination_types = []
|
||||||
@@ -360,8 +391,6 @@ def get_topology_data(
|
|||||||
cable_ids = DefaultDict(dict)
|
cable_ids = DefaultDict(dict)
|
||||||
interface_ids = DefaultDict(dict)
|
interface_ids = DefaultDict(dict)
|
||||||
|
|
||||||
ignore_cable_type = individualOptions.ignore_cable_type
|
|
||||||
|
|
||||||
device_ids = [d.pk for d in queryset]
|
device_ids = [d.pk for d in queryset]
|
||||||
site_ids = [d.site_id for d in queryset]
|
site_ids = [d.site_id for d in queryset]
|
||||||
|
|
||||||
@@ -393,8 +422,8 @@ def get_topology_data(
|
|||||||
|
|
||||||
if show_circuit:
|
if show_circuit:
|
||||||
circuit_terminations = CircuitTermination.objects.filter(
|
circuit_terminations = CircuitTermination.objects.filter(
|
||||||
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
|
Q(_site_id__in=site_ids) | Q(_provider_network__isnull=False)
|
||||||
).prefetch_related("provider_network", "circuit")
|
)
|
||||||
for circuit_termination in circuit_terminations:
|
for circuit_termination in circuit_terminations:
|
||||||
circuit_termination: CircuitTermination
|
circuit_termination: CircuitTermination
|
||||||
if (
|
if (
|
||||||
@@ -415,14 +444,14 @@ def get_topology_data(
|
|||||||
termination_b = create_circuit_termination(
|
termination_b = create_circuit_termination(
|
||||||
circuit_termination.cable.b_terminations[0]
|
circuit_termination.cable.b_terminations[0]
|
||||||
)
|
)
|
||||||
elif circuit_termination.provider_network is not None:
|
elif circuit_termination.termination is not None:
|
||||||
if (
|
if (
|
||||||
circuit_termination.provider_network_id
|
circuit_termination.termination_id
|
||||||
not in nodes_provider_networks
|
not in nodes_provider_networks
|
||||||
):
|
):
|
||||||
nodes_provider_networks[
|
nodes_provider_networks[
|
||||||
circuit_termination.provider_network.pk
|
circuit_termination.termination.pk
|
||||||
] = circuit_termination.provider_network
|
] = circuit_termination.termination
|
||||||
|
|
||||||
if bool(termination_a) and bool(termination_b):
|
if bool(termination_a) and bool(termination_b):
|
||||||
circuit_model = {
|
circuit_model = {
|
||||||
@@ -436,6 +465,7 @@ def get_topology_data(
|
|||||||
circuit=circuit_model,
|
circuit=circuit_model,
|
||||||
termination_a=termination_a,
|
termination_a=termination_a,
|
||||||
termination_b=termination_b,
|
termination_b=termination_b,
|
||||||
|
straight_cables=straight_cables,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -462,7 +492,7 @@ def get_topology_data(
|
|||||||
] = circuit_termination.circuit
|
] = circuit_termination.circuit
|
||||||
|
|
||||||
for d in nodes_circuits.values():
|
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:
|
if show_power:
|
||||||
power_panels_ids = PowerPanel.objects.filter(
|
power_panels_ids = PowerPanel.objects.filter(
|
||||||
@@ -505,6 +535,7 @@ def get_topology_data(
|
|||||||
termination_a=termination_a,
|
termination_a=termination_a,
|
||||||
termination_b=termination_b,
|
termination_b=termination_b,
|
||||||
power=True,
|
power=True,
|
||||||
|
straight_cables=straight_cables,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -512,10 +543,10 @@ def get_topology_data(
|
|||||||
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
|
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
|
||||||
|
|
||||||
for d in nodes_powerfeed.values():
|
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():
|
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:
|
if show_logical_connections:
|
||||||
interfaces = Interface.objects.filter(
|
interfaces = Interface.objects.filter(
|
||||||
@@ -543,7 +574,7 @@ def get_topology_data(
|
|||||||
edge_ids += 1
|
edge_ids += 1
|
||||||
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
|
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 }
|
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[interface.device.id] = interface.device
|
||||||
nodes_devices[destination.device.id] = destination.device
|
nodes_devices[destination.device.id] = destination.device
|
||||||
|
|
||||||
@@ -553,11 +584,11 @@ def get_topology_data(
|
|||||||
).select_related("termination_type")
|
).select_related("termination_type")
|
||||||
|
|
||||||
for link in links:
|
for link in links:
|
||||||
if link.termination_type.name in ignore_cable_type:
|
if link.termination_type.model in ignore_cable_type:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Normal device cables
|
# Normal device cables
|
||||||
if link.termination_type.name in supported_termination_types:
|
if link.termination_type.model in supported_termination_types:
|
||||||
complete_link = False
|
complete_link = False
|
||||||
if link.cable_end == "A":
|
if link.cable_end == "A":
|
||||||
if link.cable_id not in cable_ids:
|
if link.cable_id not in cable_ids:
|
||||||
@@ -623,6 +654,7 @@ def get_topology_data(
|
|||||||
cable=link.cable,
|
cable=link.cable,
|
||||||
termination_a=termination_a,
|
termination_a=termination_a,
|
||||||
termination_b=termination_b,
|
termination_b=termination_b,
|
||||||
|
straight_cables=straight_cables,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -662,6 +694,7 @@ def get_topology_data(
|
|||||||
termination_a=termination_a,
|
termination_a=termination_a,
|
||||||
termination_b=termination_b,
|
termination_b=termination_b,
|
||||||
wireless=wireless,
|
wireless=wireless,
|
||||||
|
straight_cables=straight_cables,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -671,6 +704,12 @@ def get_topology_data(
|
|||||||
options['group_racks'] = 'on'
|
options['group_racks'] = 'on'
|
||||||
if group_sites:
|
if group_sites:
|
||||||
options['group_sites'] = 'on'
|
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:
|
for qs_device in queryset:
|
||||||
if qs_device.pk not in nodes_devices and show_unconnected:
|
if qs_device.pk not in nodes_devices and show_unconnected:
|
||||||
@@ -679,7 +718,7 @@ def get_topology_data(
|
|||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
for d in nodes_devices.values():
|
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["nodes"] = nodes
|
||||||
results["edges"] = edges
|
results["edges"] = edges
|
||||||
@@ -697,7 +736,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
self.filterset = DeviceFilterSet
|
self.filterset = DeviceFilterSet
|
||||||
self.queryset = Device.objects.all().select_related(
|
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
|
||||||
"device_type", "role"
|
"device_type", "role"
|
||||||
)
|
)
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
@@ -710,7 +749,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
|
||||||
filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors = get_query_settings(request)
|
filter_id, 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
|
# Read options from saved filters as NetBox does not handle custom plugin filters
|
||||||
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
||||||
@@ -718,7 +757,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||||
saved_filter_params = getattr(saved_filter, 'parameters')
|
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 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_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_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_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||||
@@ -728,14 +769,21 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
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_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_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 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
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
pass
|
pass
|
||||||
except Exception as inst:
|
except Exception as inst:
|
||||||
print(type(inst))
|
print(type(inst))
|
||||||
|
|
||||||
if "group" not in request.GET:
|
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:
|
else:
|
||||||
group_id = request.GET["group"]
|
group_id = request.GET["group"]
|
||||||
|
|
||||||
@@ -743,6 +791,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
topo_data = get_topology_data(
|
topo_data = get_topology_data(
|
||||||
queryset=self.queryset,
|
queryset=self.queryset,
|
||||||
individualOptions=individualOptions,
|
individualOptions=individualOptions,
|
||||||
|
ignore_cable_type=ignore_cable_type,
|
||||||
save_coords=save_coords,
|
save_coords=save_coords,
|
||||||
show_unconnected=show_unconnected,
|
show_unconnected=show_unconnected,
|
||||||
show_cables=show_cables,
|
show_cables=show_cables,
|
||||||
@@ -755,17 +804,24 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
group_sites=group_sites,
|
group_sites=group_sites,
|
||||||
group_locations=group_locations,
|
group_locations=group_locations,
|
||||||
group_racks=group_racks,
|
group_racks=group_racks,
|
||||||
|
group_virtualchassis=group_virtualchassis,
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
|
straight_cables=straight_cables,
|
||||||
|
grid_size=grid_size,
|
||||||
|
node_label_items=node_label_items,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# No GET-Request in URL. We most likely came here from the navigation menu.
|
# 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_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)
|
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 = QueryDict(mutable=True)
|
||||||
q.setlist("role_id", list(preselected_device_roles))
|
q.setlist("role_id", list(preselected_device_roles))
|
||||||
q.setlist("tag", list(preselected_tags))
|
q.setlist("tag", list(preselected_tags))
|
||||||
|
q.setlist("ignore_cable_type", ignore_cable_type)
|
||||||
|
|
||||||
if individualOptions.save_coords: q['save_coords'] = "True"
|
if individualOptions.save_coords: q['save_coords'] = "True"
|
||||||
if individualOptions.show_unconnected: q['show_unconnected'] = "True"
|
if individualOptions.show_unconnected: q['show_unconnected'] = "True"
|
||||||
@@ -779,6 +835,13 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
if individualOptions.group_sites: q['group_sites'] = "True"
|
if individualOptions.group_sites: q['group_sites'] = "True"
|
||||||
if individualOptions.group_locations: q['group_locations'] = "True"
|
if individualOptions.group_locations: q['group_locations'] = "True"
|
||||||
if individualOptions.group_racks: q['group_racks'] = "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.draw_default_layout:
|
if individualOptions.draw_default_layout:
|
||||||
q['draw_init'] = "True"
|
q['draw_init'] = "True"
|
||||||
else:
|
else:
|
||||||
@@ -1136,7 +1199,11 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
|||||||
'group_sites': queryset.group_sites,
|
'group_sites': queryset.group_sites,
|
||||||
'group_locations': queryset.group_locations,
|
'group_locations': queryset.group_locations,
|
||||||
'group_racks': queryset.group_racks,
|
'group_racks': queryset.group_racks,
|
||||||
|
'group_virtualchassis': queryset.group_virtualchassis,
|
||||||
'draw_default_layout': queryset.draw_default_layout,
|
'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(', ')),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ long_description = readme.read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="netbox-topology-views",
|
name="netbox-topology-views",
|
||||||
version="4.0.0",
|
version="4.2.1",
|
||||||
description="An NetBox plugin to create Topology maps",
|
description="An NetBox plugin to create Topology maps",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
Reference in New Issue
Block a user