Update to support netbox v3.1 (#82)
* init for v3.1 * added coords + bump version * fix filter + added preselected roles * added draw_default_layout * upgraded packages * fixed edge/node labels * fixed enable_circuit_terminations * added filter for unconnected devices * added ignore_cable_type * fixed old settings using a string instead of array * removed distutils * fixed coords check * added coor saving * added doc img * updated docs * bump version * bump version
This commit is contained in:
@@ -8,7 +8,7 @@ Support to filter on name, site, tag and device role.
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||

|
||||
|
||||
## Install
|
||||
|
||||
@@ -35,6 +35,7 @@ Then run `python3 manage.py collectstatic --no-input`
|
||||
|
||||
| netbox version | netbox-topology-views version |
|
||||
| ------------- |-------------|
|
||||
| >= 3.1.8 | >= v1.0.0a1 |
|
||||
| >= 2.11.1 | >= v0.5.3 |
|
||||
| >= 2.10.0 | >= v0.5.0 |
|
||||
| < 2.10.0 | =< v0.4.10 |
|
||||
@@ -55,20 +56,21 @@ Example:
|
||||
```
|
||||
PLUGINS_CONFIG = {
|
||||
'netbox_topology_views': {
|
||||
'device_img': 'router,switch,firewall',
|
||||
'preselected_device_roles': 'Router, Firewall'
|
||||
'device_img': ['router','switch', 'firewall'],
|
||||
'preselected_device_roles': ['Router', 'Firewall']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default value | Description |
|
||||
| ------------- |-------------| -----|
|
||||
| device_img | 'access-switch,core-switch,firewall,router,distribution-switch,backup,storage,wan-network,wireless-ap,server,internal-switch,isp-cpe-material,non-racked-devices,power-units' | The slug of the device roles that you have a image for. |
|
||||
| preselected_device_roles | 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP' | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
|
||||
| device_img |['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage,wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'] | The slug of the device roles that you have a image for. |
|
||||
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
|
||||
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
|
||||
| ignore_cable_type | 'power outlet,power port' | The cable types that you want to ignore in the views |
|
||||
| preselected_tags | '' | The name of tags you want to preload |
|
||||
| ignore_cable_type | ['power outlet', 'power port'] | The cable types that you want to ignore in the views |
|
||||
| preselected_tags | '[]' | The name of tags you want to preload |
|
||||
| enable_circuit_terminations | False | (bool) Set to true if you want to see circuit terminations in the topology |
|
||||
| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) |
|
||||
|
||||
### Custom Images
|
||||
|
||||
@@ -78,7 +80,7 @@ If you add your own image you also need to add the slug to the `device_img` sett
|
||||
|
||||
## Use
|
||||
|
||||
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/topology-views/` to view your topologies
|
||||
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies
|
||||
|
||||
### Update
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -4,18 +4,19 @@ class TopologyViewsConfig(PluginConfig):
|
||||
name = 'netbox_topology_views'
|
||||
verbose_name = 'Topology views'
|
||||
description = 'An plugin to render topology maps'
|
||||
version = '0.5.3'
|
||||
version = '1.0.0'
|
||||
author = 'Mattijs Vanhaverbeke'
|
||||
author_email = 'author@example.com'
|
||||
base_url = 'topology-views'
|
||||
base_url = 'netbox_topology_views'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
||||
'ignore_cable_type': 'power outlet,power port',
|
||||
'device_img': 'access-switch,core-switch,firewall,router,distribution-switch,backup,storage,wan-network,wireless-ap,server,internal-switch,isp-cpe-material,non-racked-devices,power-units',
|
||||
'preselected_device_roles': ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'],
|
||||
'ignore_cable_type': ['power outlet','power port'],
|
||||
'device_img': ['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage,wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'],
|
||||
'allow_coordinates_saving': False,
|
||||
'preselected_tags' : '',
|
||||
'enable_circuit_terminations': False
|
||||
'preselected_tags' : [],
|
||||
'enable_circuit_terminations': True,
|
||||
'draw_default_layout': False
|
||||
}
|
||||
|
||||
config = TopologyViewsConfig
|
||||
|
||||
@@ -3,18 +3,6 @@ from rest_framework.serializers import ModelSerializer
|
||||
from dcim.models import DeviceRole, Device
|
||||
from extras.models import Tag
|
||||
|
||||
class PreDeviceRoleSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ('id', 'name')
|
||||
|
||||
class PreTagSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
class TopologyDummySerializer(ModelSerializer):
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from rest_framework import routers
|
||||
from . import views
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = views.TopologyViewsRootView
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
|
||||
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
|
||||
router.register('preselecttags', views.PreSelectTagsViewSet)
|
||||
router.register('search', views.SearchViewSet, basename='search')
|
||||
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -4,7 +4,7 @@ from rest_framework.response import Response
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from .serializers import PreDeviceRoleSerializer, TopologyDummySerializer, PreTagSerializer
|
||||
from .serializers import TopologyDummySerializer
|
||||
from django.conf import settings
|
||||
|
||||
from dcim.models import DeviceRole, Device, Cable
|
||||
@@ -12,26 +12,10 @@ from circuits.models import Circuit
|
||||
from extras.models import Tag
|
||||
|
||||
|
||||
ignore_cable_type_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
|
||||
ignore_cable_type = ignore_cable_type_raw.split(",")
|
||||
|
||||
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
||||
preselected_device_roles = preselected_device_roles_raw.split(",")
|
||||
|
||||
preselected_tags_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
|
||||
preselected_tags = preselected_tags_raw.split(",")
|
||||
|
||||
class TopologyViewsRootView(APIRootView):
|
||||
def get_view_name(self):
|
||||
return 'TopologyViews'
|
||||
|
||||
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
|
||||
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
|
||||
serializer_class = PreDeviceRoleSerializer
|
||||
|
||||
class PreSelectTagsViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Tag.objects.filter(name__in=preselected_tags)
|
||||
serializer_class = PreTagSerializer
|
||||
|
||||
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Device.objects.all()
|
||||
@@ -74,200 +58,3 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||
results["status"] = "not allowed to save coords"
|
||||
return Response(results, status=500)
|
||||
|
||||
class SearchViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
def _filter(self, site, role, name, tags, region, location):
|
||||
filter_devices = Device.objects.all()
|
||||
if name is not None:
|
||||
filter_devices = filter_devices.filter(name__contains=name)
|
||||
if site is not None:
|
||||
filter_devices = filter_devices.filter(site__id__in=site)
|
||||
if role is not None:
|
||||
filter_devices = filter_devices.filter(device_role__id__in=role)
|
||||
if tags is not None:
|
||||
filter_devices = filter_devices.filter(tags__id__in=tags)
|
||||
if region is not None:
|
||||
filter_devices = filter_devices.filter(site__region__id__in=region)
|
||||
if location is not None:
|
||||
filter_devices = filter_devices.filter(location__id__in=location)
|
||||
return filter_devices
|
||||
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def search(self, request):
|
||||
name = request.query_params.get('name', None)
|
||||
if name == "":
|
||||
name = None
|
||||
|
||||
sites = request.query_params.getlist('sites[]', None)
|
||||
if sites == []:
|
||||
sites = None
|
||||
|
||||
devicerole = request.query_params.getlist('devicerole[]', None)
|
||||
if devicerole == []:
|
||||
devicerole = None
|
||||
|
||||
tags = request.query_params.getlist('tags[]', None)
|
||||
if tags == []:
|
||||
tags = None
|
||||
|
||||
regions = request.query_params.getlist('regions[]', None)
|
||||
if regions == []:
|
||||
regions = None
|
||||
|
||||
locations = request.query_params.getlist('locations[]', None)
|
||||
if locations == []:
|
||||
locations = None
|
||||
|
||||
hide_unconnected = request.query_params.get('hide_unconnected', None)
|
||||
if hide_unconnected == "":
|
||||
hide_unconnected = None
|
||||
|
||||
devices = self._filter(sites, devicerole, name, tags, regions, locations)
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
edge_ids = 0
|
||||
cable_ids = []
|
||||
circuit_ids = []
|
||||
for device in devices:
|
||||
cables = device.get_cables()
|
||||
if cables.exists():
|
||||
device_has_connections = True
|
||||
for cable in cables:
|
||||
if cable.termination_a_type.name != "circuit termination" and cable.termination_b_type.name != "circuit termination":
|
||||
if cable.id not in cable_ids:
|
||||
if cable.termination_a_type.name not in ignore_cable_type and cable.termination_b_type.name not in ignore_cable_type:
|
||||
cable_ids.append(cable.id)
|
||||
edge_ids += 1
|
||||
|
||||
cable_a_dev_name = cable.termination_a.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device A name unknown"
|
||||
cable_a_name = cable.termination_a.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable A name unknown"
|
||||
cable_b_dev_name = cable.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = cable.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["from"] = cable.termination_a.device.id
|
||||
edge["to"] = cable.termination_b.device.id
|
||||
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||
if cable.color != "":
|
||||
edge["color"] = "#" + cable.color
|
||||
edges.append(edge)
|
||||
else:
|
||||
if cable.termination_a_type.name == "circuit termination":
|
||||
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
|
||||
if cable.termination_a.circuit.id not in circuit_ids:
|
||||
circuit_ids.append(cable.termination_a.circuit.id)
|
||||
edge_ids += 1
|
||||
|
||||
cable_b_dev_name = cable.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = cable.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["to"] = cable.termination_b.device.id
|
||||
edge["dashes"] = True
|
||||
title = ""
|
||||
|
||||
title += "Circuit provider: " + cable.termination_a.circuit.provider.name + "<br>"
|
||||
title += "Termination between <br>"
|
||||
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
|
||||
# To Many if's
|
||||
if cable.termination_a.circuit.termination_a is not None:
|
||||
if cable.termination_a.circuit.termination_a.cable is not None:
|
||||
if cable.termination_a.circuit.termination_a.cable.id != cable.id:
|
||||
if cable.termination_a.circuit.termination_a.cable.termination_b is not None:
|
||||
if cable.termination_a.circuit.termination_a.cable.termination_b.device is not None:
|
||||
edge["from"] = cable.termination_a.circuit.termination_a.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = cable.termination_a.circuit.termination_a.cable.termination_b.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device B name unknown"
|
||||
cable_b_name = cable.termination_a.circuit.termination_a.cable.termination_b.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable B name unknown"
|
||||
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||
edge["title"] = title
|
||||
edges.append(edge)
|
||||
# To Many if's
|
||||
if cable.termination_a.circuit.termination_z is not None:
|
||||
if cable.termination_a.circuit.termination_z.cable is not None:
|
||||
if cable.termination_a.circuit.termination_z.cable.id != cable.id:
|
||||
if cable.termination_a.circuit.termination_z.cable.termination_b is not None:
|
||||
if cable.termination_a.circuit.termination_z.cable.termination_b.device is not None:
|
||||
edge["from"] = cable.termination_a.circuit.termination_z.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = cable.termination_a.circuit.termination_z.cable.termination_b.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device B name unknown"
|
||||
cable_a_name = cable.termination_a.circuit.termination_z.cable.termination_b.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable B name unknown"
|
||||
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||
edge["title"] = title
|
||||
edges.append(edge)
|
||||
else:
|
||||
device_has_connections = False
|
||||
|
||||
if hide_unconnected == 'false' or (hide_unconnected == 'true' and device_has_connections is True):
|
||||
dev_name = device.name
|
||||
if dev_name is None:
|
||||
dev_name = "device name unknown"
|
||||
|
||||
node_content = ""
|
||||
|
||||
if device.device_type.display_name is not None:
|
||||
node_content += "<tr><th>Type: </th><td>" + device.device_type.display_name + "</td></tr>"
|
||||
if device.device_role.name is not None:
|
||||
node_content += "<tr><th>Role: </th><td>" + device.device_role.name + "</td></tr>"
|
||||
if device.serial != "":
|
||||
node_content += "<tr><th>Serial: </th><td>" + device.serial + "</td></tr>"
|
||||
if device.primary_ip is not None:
|
||||
node_content += "<tr><th>IP Address: </th><td>" + str(device.primary_ip.address) + "</td></tr>"
|
||||
|
||||
dev_title = "<table> %s </table>" % (node_content)
|
||||
|
||||
node = {}
|
||||
node["id"] = device.id
|
||||
node["name"] = dev_name
|
||||
node["label"] = dev_name
|
||||
node["title"] = dev_title
|
||||
node["shape"] = 'image'
|
||||
if device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
|
||||
node["image"] = '../../static/netbox_topology_views/img/' + device.device_role.slug + ".png"
|
||||
else:
|
||||
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
|
||||
|
||||
if device.device_role.color != "":
|
||||
node["color.border"] = "#" + device.device_role.color
|
||||
|
||||
if "coordinates" in device.custom_field_data:
|
||||
if device.custom_field_data["coordinates"] != "":
|
||||
cords = device.custom_field_data["coordinates"].split(";")
|
||||
node["x"] = int(cords[0])
|
||||
node["y"] = int(cords[1])
|
||||
node["physics"] = False
|
||||
|
||||
nodes.append(node)
|
||||
|
||||
results = {}
|
||||
results["nodes"] = nodes
|
||||
results["edges"] = edges
|
||||
|
||||
return Response(results)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import django_filters
|
||||
from extras.filters import TagFilter
|
||||
from dcim.models import Device, DeviceRole, Region, Site, Location
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from django.db.models import Q
|
||||
|
||||
class DeviceFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_role_id',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Location (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', ]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -0,0 +1,60 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.models import Tag
|
||||
from dcim.models import Device, Site, Region, DeviceRole, Location
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, FilterForm)
|
||||
allow_coordinates_saving = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
|
||||
|
||||
class DeviceFilterForm(FilterForm):
|
||||
model = Device
|
||||
field_groups = [
|
||||
['q', 'hide_unconnected', 'save_coords'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['device_role_id'],
|
||||
['tag'],
|
||||
]
|
||||
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
device_role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Device Role')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Location')
|
||||
)
|
||||
|
||||
hide_unconnected = forms.BooleanField(
|
||||
label=_("Hide Unconnected"),
|
||||
required=False,
|
||||
initial=False)
|
||||
|
||||
save_coords = forms.BooleanField(
|
||||
label=_("Save Coordinates"),
|
||||
required=False,
|
||||
disabled=(not allow_coordinates_saving),
|
||||
initial=False)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
@@ -5,6 +5,13 @@ menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_topology_views:home',
|
||||
link_text='Topology',
|
||||
buttons=()
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:home',
|
||||
title='Topology View',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=[],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
#visgraph {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
var graph = null;
|
||||
var container = null;
|
||||
|
||||
function iniPlotboxFull() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
container = document.getElementById('fullvisgraph');
|
||||
startRender();
|
||||
}, false);
|
||||
}
|
||||
|
||||
function startRender() {
|
||||
var url = location.search;
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "../../api/plugins/topology-views/search/search/" + url,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
success: function (data_result, status, xhr) {
|
||||
graph = null;
|
||||
nodes = new vis.DataSet();
|
||||
edges = new vis.DataSet();
|
||||
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||
$.each(data_result["nodes"], function (index, device) {
|
||||
nodes.add(device);
|
||||
});
|
||||
$.each(data_result["edges"], function (index, edge) {
|
||||
edges.add(edge);
|
||||
});
|
||||
graph.fit();
|
||||
canvas = document.getElementById('fullvisgraph').getElementsByTagName('canvas')[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -34,344 +34,101 @@ var options = {
|
||||
};
|
||||
var selected_regions = [];
|
||||
var selected_sites = [];
|
||||
var coord_save_checkbox = null;
|
||||
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
|
||||
function htmlTitle(html) {
|
||||
container = document.createElement("div");
|
||||
container.innerHTML = html;
|
||||
return container;
|
||||
}
|
||||
|
||||
function addEdge(item) {
|
||||
item.title = htmlTitle( item.title );
|
||||
edges.add(item);
|
||||
}
|
||||
|
||||
function addNode(item) {
|
||||
item.title = htmlTitle( item.title );
|
||||
nodes.add(item);
|
||||
}
|
||||
|
||||
function iniPlotboxIndex() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
csrftoken = getCookie('csrftoken');
|
||||
container = document.getElementById('visgraph');
|
||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
||||
startLoadSearchBar();
|
||||
handleButtonPress();
|
||||
handleLoadData();
|
||||
downloadButton = document.getElementById('btnDownloadImage');
|
||||
btnFullView = document.getElementById('btnFullView');
|
||||
coord_save_checkbox = document.getElementById('id_save_coords');
|
||||
}, false);
|
||||
}
|
||||
|
||||
function startLoadSearchBar() {
|
||||
$('#device-roles').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/dcim/device-roles/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#sites').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: function (params) {
|
||||
var base_url = "../../api/dcim/sites/?brief=true";
|
||||
if (selected_regions.length == 0) {
|
||||
return base_url;
|
||||
}
|
||||
else {
|
||||
for (var i = 0; i < selected_regions.length; i++) {
|
||||
var tmp = "®ion_id=" + selected_regions[i];
|
||||
base_url = base_url + tmp;
|
||||
function handleLoadData() {
|
||||
if (topology_data !== null) {
|
||||
graph = null;
|
||||
nodes = new vis.DataSet();
|
||||
edges = new vis.DataSet();
|
||||
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||
|
||||
topology_data.edges.forEach(addEdge);
|
||||
topology_data.nodes.forEach(addNode);
|
||||
|
||||
graph.fit();
|
||||
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||
|
||||
graph.on('afterDrawing', function () {
|
||||
var image = canvas.toDataURL(MIME_TYPE);
|
||||
downloadButton.href = image;
|
||||
downloadButton.download = "topology";
|
||||
});
|
||||
|
||||
graph.on("dragEnd", function (params) {
|
||||
dragged = this.getPositions(params.nodes);
|
||||
|
||||
if (coord_save_checkbox.checked) {
|
||||
if (Object.keys(dragged).length !== 0) {
|
||||
for (dragged_device in dragged) {
|
||||
var node_id = dragged_device;
|
||||
|
||||
var url = "/api/plugins/netbox_topology_views/save-coords/save_coords/";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("PATCH", url);
|
||||
xhr.setRequestHeader('X-CSRFToken', csrftoken );
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
console.log(xhr.status);
|
||||
console.log(xhr.responseText);
|
||||
}};
|
||||
|
||||
var data = JSON.stringify({
|
||||
'node_id': node_id,
|
||||
'x': dragged[node_id].x,
|
||||
'y': dragged[node_id].y});
|
||||
|
||||
xhr.send(data);
|
||||
}
|
||||
return base_url;
|
||||
}
|
||||
|
||||
},
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#sites').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
var index = selected_sites.indexOf(data.id.toString());
|
||||
if (index == -1) {
|
||||
selected_sites.push(data.id.toString());
|
||||
}
|
||||
});
|
||||
$('#sites').on('select2:unselect', function (e) {
|
||||
var data = e.params.data;
|
||||
var index = selected_sites.indexOf(data.id.toString());
|
||||
if (index > -1) {
|
||||
selected_sites.splice(index, 1);
|
||||
}
|
||||
});
|
||||
$('#sites').on('select2:clear', function (e) {
|
||||
selected_sites = [];
|
||||
});
|
||||
|
||||
$('#tags').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/extras/tags/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#regions').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/dcim/regions/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#regions').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
var index = selected_regions.indexOf(data.id.toString());
|
||||
if (index == -1) {
|
||||
selected_regions.push(data.id.toString());
|
||||
}
|
||||
});
|
||||
$('#regions').on('select2:unselect', function (e) {
|
||||
var data = e.params.data;
|
||||
var index = selected_regions.indexOf(data.id.toString());
|
||||
if (index > -1) {
|
||||
selected_regions.splice(index, 1);
|
||||
}
|
||||
});
|
||||
$('#regions').on('select2:clear', function (e) {
|
||||
selected_regions = [];
|
||||
});
|
||||
|
||||
$('#locations').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: function (params) {
|
||||
var base_url = "../../api/dcim/locations/?brief=true";
|
||||
if (selected_sites.length == 0) {
|
||||
return base_url;
|
||||
}
|
||||
else {
|
||||
for (var i = 0; i < selected_sites.length; i++) {
|
||||
var tmp = "&site_id=" + selected_sites[i];
|
||||
base_url = base_url + tmp;
|
||||
}
|
||||
return base_url;
|
||||
}
|
||||
|
||||
},
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var deviceRolesSelect = $('#device-roles');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '../../api/plugins/topology-views/preselectdeviceroles/'
|
||||
}).then(function (data) {
|
||||
$.each(data.results, function (index, device_role_to_preload) {
|
||||
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
||||
deviceRolesSelect.append(option).trigger('change');
|
||||
deviceRolesSelect.trigger({
|
||||
type: 'select2:select',
|
||||
params: {
|
||||
data: device_role_to_preload
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var tagsSelect = $('#tags');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '../../api/plugins/topology-views/preselecttags/'
|
||||
}).then(function (data) {
|
||||
$.each(data.results, function (index, tags_to_preload) {
|
||||
var option = new Option(tags_to_preload.name, tags_to_preload.id, true, true);
|
||||
tagsSelect.append(option).trigger('change');
|
||||
tagsSelect.trigger({
|
||||
type: 'select2:select',
|
||||
params: {
|
||||
data: tags_to_preload
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonPress() {
|
||||
$("#search-form").submit(function (event) {
|
||||
$("#status").html('<span class="label label-info">Loading data</span>');
|
||||
event.preventDefault();
|
||||
var value = $("#name").val();
|
||||
var value2 = $("#device-roles").val();
|
||||
var value3 = $("#sites").val();
|
||||
var value4 = $("#tags").val();
|
||||
var value5 = $("#regions").val();
|
||||
var value6 = $('#checkHideUnconnected').is(":checked");
|
||||
var value7 = $("#locations").val();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "../../api/plugins/topology-views/search/search/",
|
||||
data: {
|
||||
'name': value,
|
||||
'devicerole[]': value2,
|
||||
'sites[]': value3,
|
||||
'tags[]': value4,
|
||||
'regions[]': value5,
|
||||
'hide_unconnected': value6,
|
||||
'locations[]': value7
|
||||
},
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
beforeSend: function(){
|
||||
new_url = this.url.split("../../api/plugins/topology-views/search/search/?");
|
||||
new_url = new_url[1];
|
||||
new_url = "../../plugins/topology-views/full?" + new_url
|
||||
|
||||
btnFullView.classList.remove("disabled");
|
||||
btnFullView.href = new_url;
|
||||
},
|
||||
success: function (data_result, status, xhr) {
|
||||
$("#status").html('<span class="label label-info">Drawing network</span>');
|
||||
graph = null;
|
||||
nodes = new vis.DataSet();
|
||||
edges = new vis.DataSet();
|
||||
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||
$.each(data_result["nodes"], function (index, device) {
|
||||
nodes.add(device);
|
||||
});
|
||||
$.each(data_result["edges"], function (index, edge) {
|
||||
edges.add(edge);
|
||||
});
|
||||
graph.fit();
|
||||
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||
|
||||
graph.on('afterDrawing', function () {
|
||||
$("#status").html('<span class="label label-success">Ready</span>');
|
||||
var image = canvas.toDataURL(MIME_TYPE);
|
||||
downloadButton.href = image;
|
||||
downloadButton.download = "topology";
|
||||
});
|
||||
|
||||
graph.on("dragEnd", function (params) {
|
||||
dragged = this.getPositions(params.nodes);
|
||||
$.each(dragged, function (node_id, coordinates) {
|
||||
$("#coordstatus").html('');
|
||||
if ($('#checkSaveCoordinates').is(":checked")) {
|
||||
//nodes.update({ id: node_id, physics: false });
|
||||
$.ajax({
|
||||
url: "../../api/plugins/topology-views/save-coords/save_coords/",
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
processData: false,
|
||||
data: JSON.stringify({
|
||||
'node_id': node_id,
|
||||
'x': coordinates.x,
|
||||
'y': coordinates.y
|
||||
}),
|
||||
error: function (error_result) {
|
||||
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
|
||||
},
|
||||
success: function (data_result) {
|
||||
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
error: function (error_result) {
|
||||
$("#status").html('<span class="label label-warning">Something went wrong</span>');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||
|
||||
|
||||
<div class="panel-body">
|
||||
<div id="fullvisgraph" class=""></div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'jquery/jquery-3.5.1.min.js' %}"></script>
|
||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||
|
||||
<script type="application/javascript"> iniPlotboxFull()</script>
|
||||
{% endblock %}
|
||||
@@ -1,103 +1,74 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load perms %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
{% block title %}Topology Views{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Network</strong> <span id="status"></span>
|
||||
<div class="buttons pull-right">
|
||||
<a id="btnDownloadImage" class="btn btn-xs btn-info">
|
||||
<i class="mdi mdi-download"></i>
|
||||
</a>
|
||||
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
|
||||
<i class="mdi mdi-share"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
<a id="btnDownloadImage" class="btn btn-sm btn-info">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% 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">
|
||||
Network
|
||||
</button>
|
||||
</li>
|
||||
{% if filter_form %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
Filters
|
||||
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock tab_items %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||
<div class="panel-body">
|
||||
<div id="visgraph" class=""></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var topology_data = {{ topology_data | safe }};
|
||||
</script>
|
||||
</div>
|
||||
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="search-form" class="form" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" class="form-control" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-roles">Device roles</label>
|
||||
<select class="form-control" multiple="multiple" id="device-roles"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="regions">Regions</label>
|
||||
<select class="form-control" multiple="multiple" id="regions"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sites">Sites</label>
|
||||
<select class="form-control" multiple="multiple" id="sites"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="locations">Locations</label>
|
||||
<select class="form-control" multiple="multiple" id="locations"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<select class="form-control" multiple="multiple" id="tags"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="checkHideUnconnected">
|
||||
<label class="custom-control-label" for="checkHideUnconnected">Hide Unconnected</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mb-4">
|
||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if config.allow_coordinates_saving == True %}
|
||||
<div class="panel panel-default mt-3">
|
||||
<div class="panel-heading">
|
||||
<strong>Settings</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="checkSaveCoordinates">
|
||||
<label class="custom-control-label" for="checkSaveCoordinates">Save coordinates</label>
|
||||
</div>
|
||||
<div>
|
||||
<span id="coordstatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||
|
||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
|
||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||
|
||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
@@ -7,5 +7,4 @@ from . import views
|
||||
# a specific view so that it can be accessed by users.
|
||||
urlpatterns = (
|
||||
path('', views.TopologyHomeView.as_view(), name='home'),
|
||||
path('full', views.TopologyFullView.as_view(), name='full'),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,167 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.db.models import Q
|
||||
from django.views.generic import View
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from .forms import DeviceFilterForm
|
||||
from .filters import DeviceFilterSet
|
||||
|
||||
import json
|
||||
|
||||
from dcim.models import Device, Cable, DeviceRole, DeviceType
|
||||
from extras.models import Tag
|
||||
|
||||
def get_topology_data(queryset, hide_unconnected):
|
||||
nodes = []
|
||||
nodes_ids = []
|
||||
edges = []
|
||||
edge_ids = 0
|
||||
cable_ids = []
|
||||
circuit_ids = []
|
||||
if not queryset:
|
||||
return None
|
||||
|
||||
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
|
||||
|
||||
device_ids = [d.id for d in queryset]
|
||||
|
||||
for qs_device in queryset:
|
||||
device_has_connections = False
|
||||
|
||||
links_device = Cable.objects.filter(Q(_termination_a_device_id=qs_device.id) | Q(_termination_b_device_id=qs_device.id) )
|
||||
for link_from in links_device:
|
||||
if link_from.termination_a_type.name != "circuit termination" and link_from.termination_b_type.name != "circuit termination":
|
||||
if link_from.termination_a_type.name not in ignore_cable_type and link_from.termination_b_type.name not in ignore_cable_type:
|
||||
if link_from.id not in cable_ids:
|
||||
if link_from.termination_a.device.id in device_ids and link_from.termination_b.device.id in device_ids:
|
||||
device_has_connections = True
|
||||
cable_ids.append(link_from.id)
|
||||
edge_ids += 1
|
||||
cable_a_dev_name = link_from.termination_a.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device A name unknown"
|
||||
cable_a_name = link_from.termination_a.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable A name unknown"
|
||||
cable_b_dev_name = link_from.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = link_from.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["from"] = link_from.termination_a.device.id
|
||||
edge["to"] = link_from.termination_b.device.id
|
||||
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||
if link_from.color != "":
|
||||
edge["color"] = "#" + link_from.color
|
||||
edges.append(edge)
|
||||
else:
|
||||
if link_from.termination_a.device.id in device_ids and link_from.termination_b.device.id in device_ids:
|
||||
device_has_connections = True
|
||||
else:
|
||||
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
|
||||
if link_from.termination_a.circuit.id not in circuit_ids:
|
||||
circuit_ids.append(link_from.termination_a.circuit.id)
|
||||
edge_ids += 1
|
||||
|
||||
cable_b_dev_name = link_from.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = link_from.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["to"] = link_from.termination_b.device.id
|
||||
edge["dashes"] = True
|
||||
title = ""
|
||||
|
||||
title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>"
|
||||
title += "Termination between <br>"
|
||||
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
|
||||
|
||||
if link_from.termination_a.circuit.termination_a is not None and link_from.termination_a.circuit.termination_a.cable is not None and link_from.termination_a.circuit.termination_a.cable.id != link_from.id and link_from.termination_a.circuit.termination_a.cable.termination_b is not None and link_from.termination_a.circuit.termination_a.cable.termination_b.device is not None:
|
||||
edge["from"] = link_from.termination_a.circuit.termination_a.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = link_from.termination_a.circuit.termination_a.cable.termination_b.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device B name unknown"
|
||||
cable_b_name = link_from.termination_a.circuit.termination_a.cable.termination_b.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable B name unknown"
|
||||
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||
edge["title"] = title
|
||||
edges.append(edge)
|
||||
|
||||
if link_from.termination_a.circuit.termination_z is not None and link_from.termination_a.circuit.termination_z.cable is not None and link_from.termination_a.circuit.termination_z.cable.id != link_from.id and link_from.termination_a.circuit.termination_z.cable.termination_b is not None and link_from.termination_a.circuit.termination_z.cable.termination_b.device is not None:
|
||||
edge["from"] = link_from.termination_a.circuit.termination_z.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = link_from.termination_a.circuit.termination_z.cable.termination_b.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device B name unknown"
|
||||
cable_a_name = link_from.termination_a.circuit.termination_z.cable.termination_b.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable B name unknown"
|
||||
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||
edge["title"] = title
|
||||
edges.append(edge)
|
||||
|
||||
if qs_device.id not in nodes_ids:
|
||||
if hide_unconnected == None or (hide_unconnected is True and device_has_connections is True):
|
||||
nodes_ids.append(qs_device.id)
|
||||
|
||||
dev_name = qs_device.name
|
||||
if dev_name is None:
|
||||
dev_name = "device name unknown"
|
||||
|
||||
node_content = ""
|
||||
|
||||
if qs_device.device_type is not None:
|
||||
node_content += "<tr><th>Type: </th><td>" + qs_device.device_type.model + "</td></tr>"
|
||||
if qs_device.device_role.name is not None:
|
||||
node_content += "<tr><th>Role: </th><td>" + qs_device.device_role.name + "</td></tr>"
|
||||
if qs_device.serial != "":
|
||||
node_content += "<tr><th>Serial: </th><td>" + qs_device.serial + "</td></tr>"
|
||||
if qs_device.primary_ip is not None:
|
||||
node_content += "<tr><th>IP Address: </th><td>" + str(qs_device.primary_ip.address) + "</td></tr>"
|
||||
|
||||
dev_title = "<table> %s </table>" % (node_content)
|
||||
|
||||
node = {}
|
||||
node["id"] = qs_device.id
|
||||
node["name"] = dev_name
|
||||
node["label"] = dev_name
|
||||
node["title"] = dev_title
|
||||
node["shape"] = 'image'
|
||||
if qs_device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
|
||||
node["image"] = '../../static/netbox_topology_views/img/' + qs_device.device_role.slug + ".png"
|
||||
else:
|
||||
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
|
||||
|
||||
if qs_device.device_role.color != "":
|
||||
node["color.border"] = "#" + qs_device.device_role.color
|
||||
|
||||
if "coordinates" in qs_device.custom_field_data:
|
||||
if qs_device.custom_field_data["coordinates"] is not None:
|
||||
if ';' in qs_device.custom_field_data["coordinates"]:
|
||||
cords = qs_device.custom_field_data["coordinates"].split(";")
|
||||
node["x"] = int(cords[0])
|
||||
node["y"] = int(cords[1])
|
||||
node["physics"] = False
|
||||
nodes.append(node)
|
||||
|
||||
results = {}
|
||||
results["nodes"] = nodes
|
||||
results["edges"] = edges
|
||||
return results
|
||||
|
||||
|
||||
class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
permission_required = ('dcim.view_site', 'dcim.view_device')
|
||||
@@ -9,13 +170,38 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
Show the home page
|
||||
"""
|
||||
def get(self, request):
|
||||
return render(request, 'netbox_topology_views/index.html')
|
||||
self.filterset = DeviceFilterSet
|
||||
self.queryset = Device.objects.all()
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
topo_data = None
|
||||
|
||||
class TopologyFullView(PermissionRequiredMixin, View):
|
||||
permission_required = ('dcim.view_site', 'dcim.view_device')
|
||||
if request.GET:
|
||||
hide_unconnected = None
|
||||
if 'hide_unconnected' in request.GET:
|
||||
if request.GET["hide_unconnected"] == "on" :
|
||||
hide_unconnected = True
|
||||
|
||||
"""
|
||||
Show the full view page
|
||||
"""
|
||||
def get(self, request):
|
||||
return render(request, 'netbox_topology_views/full.html')
|
||||
if 'draw_init' in request.GET:
|
||||
if request.GET["draw_init"].lower() == 'true':
|
||||
topo_data = get_topology_data(self.queryset, hide_unconnected)
|
||||
else:
|
||||
topo_data = get_topology_data(self.queryset, hide_unconnected)
|
||||
else:
|
||||
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
||||
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
|
||||
|
||||
q_device_role_id = DeviceRole.objects.filter(name__in=preselected_device_roles).values_list('id', flat=True)
|
||||
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list('name', flat=True)
|
||||
|
||||
q = QueryDict(mutable=True)
|
||||
q.setlist('device_role_id', list(q_device_role_id))
|
||||
q.setlist('tag', list(q_tags))
|
||||
q['draw_init'] = settings.PLUGINS_CONFIG["netbox_topology_views"]["draw_default_layout"]
|
||||
query_string = q.urlencode()
|
||||
return HttpResponseRedirect(request.path + "?" + query_string)
|
||||
|
||||
return render(request, 'netbox_topology_views/index.html' , {
|
||||
'filter_form': DeviceFilterForm(request.GET, label_suffix=''),
|
||||
'topology_data': json.dumps(topo_data)
|
||||
}
|
||||
)
|
||||
|
||||
Generated
+6113
-76
File diff suppressed because it is too large
Load Diff
+13
-12
@@ -1,26 +1,27 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "0.5.3",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"resources": "gulp build",
|
||||
"resources_dev": "gulp build_dev"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.2",
|
||||
"vis-network": "^9.1.0",
|
||||
"vis-util": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sass": "^4.1.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"keycharm": "^0.3.0",
|
||||
"moment": "^2.24.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "7.0.0",
|
||||
"vis-data": "^6.2.1",
|
||||
"vis-network": "^7.10.2",
|
||||
"vis-util": "^4.0.0"
|
||||
"gulp-uglify": "^3.0.2"
|
||||
},
|
||||
"peerDependencies": {}
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"uuid": "^8.0.0",
|
||||
"keycharm": "^0.4.0",
|
||||
"timsort": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='netbox-topology-views',
|
||||
version='0.5.3',
|
||||
version='1.0.0',
|
||||
description='An NetBox plugin to create Topology maps',
|
||||
url='https://github.com/mattieserver/netbox-topology-views',
|
||||
author='Mattijs Vanhaverbeke',
|
||||
|
||||
Reference in New Issue
Block a user