Compare commits

..
9 Commits
Author SHA1 Message Date
Mattijs Vanhaverbeke b9d3cc3c4e bump version 2022-06-30 22:06:33 +02:00
Mattijs Vanhaverbeke d4833071df added filter on device status 2022-06-30 22:03:23 +02:00
Mattijs Vanhaverbeke 877d308574 small config change 2022-06-30 19:27:11 +02:00
pycolas 83b662eac3 End to end connections (#113)
* Add hide-device-role feature

* Add hide-device-role feature

* Remove end^Cevices not in queryset

* clean up

* Fix destination.device.id test

* Query optimization

* multiple updates
2022-06-30 19:13:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 53a996edbc Bump minimist from 1.2.5 to 1.2.6 (#110)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 11:38:34 +02:00
Mattijs Vanhaverbeke 398deb5382 bump version 2022-05-10 09:28:21 +02:00
Alexander Ryazanov b5991a5f50 Improve visualization performance (substantially) (#109)
* fix: moved image generation to avoid severe performance issues

* fix: moved button finding code before data handling

* fix: fixed leftover invalid variable naming
2022-05-10 09:21:22 +02:00
mattieserver de92627176 add shadow to cables (#98)
* addes shadow to edge

* changed bg color in dark mode
2022-05-06 06:43:06 +02:00
mattieserver 6be028b9dc fix typo (#100) 2022-04-15 19:42:32 +02:00
13 changed files with 316 additions and 6252 deletions
+1
View File
@@ -67,6 +67,7 @@ PLUGINS_CONFIG = {
| ------------- |-------------| -----|
| 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|
| preselected_intermediate_dev_roles | ['Patch-panel'] | The full name of the device roles you want to display in the global view when using end-to-end connections mode. 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 |
+4 -2
View File
@@ -4,15 +4,17 @@ class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render topology maps'
version = '1.1.0'
version = '2.0.0'
author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com'
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'],
'preselected_intermediate_dev_roles' : [],
'end2end_connections': False,
'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'],
'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,
+7
View File
@@ -1,7 +1,9 @@
from cProfile import label
import django_filters
from django.db.models import Q
from dcim.models import Device, DeviceRole, Region, Site, Location
from dcim.choices import DeviceStatusChoices
from netbox.filtersets import NetBoxModelFilterSet
@@ -37,6 +39,11 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
lookup_expr='in',
label='Location (ID)',
)
status = django_filters.MultipleChoiceFilter(
choices=DeviceStatusChoices,
null_value=None
)
class Meta:
model = Device
+27 -7
View File
@@ -1,3 +1,4 @@
from cProfile import label
import imp
from django import forms
from django.conf import settings
@@ -7,23 +8,25 @@ from django.utils.translation import gettext as _
from dcim.models import Device, Site, Region, DeviceRole, Location
from django import forms
from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField)
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, MultipleChoiceField)
allow_coordinates_saving = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
end2end = settings.PLUGINS_CONFIG["netbox_topology_views"]["end2end_connections"]
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
fieldsets = (
(None, ('q', 'hide_unconnected', 'save_coords')),
(None, ('q', 'hide_unconnected', 'save_coords', 'end2end_connections')),
(None, ('tenant_group_id', 'tenant_id')),
(None, ('region_id', 'site_id', 'location_id')),
(None, ('device_role_id',)),
(None, ('tag',)),
(None, ('device_role_id', 'intermediate_dev_role_id')),
(None, ('tag', 'status')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -36,6 +39,17 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label=_('Device Role')
)
end2end_connections = forms.BooleanField(
label=_("Display end-to-end connections"),
required=False,
initial=end2end
)
intermediate_dev_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Intermediate Devices Role'),
help_text='Intermediate devices to display when using end-to-end connections mode, even if they do not match the query'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
@@ -56,11 +70,17 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"),
required=False,
initial=False)
initial=False
)
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not allow_coordinates_saving),
initial=False)
initial=False
)
status = MultipleChoiceField(
choices=DeviceStatusChoices,
required=False,
label=_("Device Status")
)
tag = TagFilterField(model)
@@ -1 +1 @@
#visgraph{height:70vh}
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}
@@ -1 +1 @@
var graph=null,container=null,downloadButton=null,MIME_TYPE="image/png",canvas=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[],coord_save_checkbox=null,htmlElement=null;function getCookie(e){var t=null;if(document.cookie&&""!==document.cookie)for(var n=document.cookie.split(";"),o=0;o<n.length;o++){var a=n[o].trim();if(a.substring(0,e.length+1)===e+"="){t=decodeURIComponent(a.substring(e.length+1));break}}return t}function htmlTitle(e){return(container=document.createElement("div")).innerHTML=e,container}function addEdge(e){e.title=htmlTitle(e.title),edges.add(e)}function addNode(e){e.title=htmlTitle(e.title),nodes.add(e)}function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){csrftoken=getCookie("csrftoken"),container=document.getElementById("visgraph"),htmlElement=document.getElementsByTagName("html")[0],handleLoadData(),downloadButton=document.getElementById("btnDownloadImage"),btnFullView=document.getElementById("btnFullView"),coord_save_checkbox=document.getElementById("id_save_coords")},!1)}function handleLoadData(){null!==topology_data&&("dark"==htmlElement.dataset.netboxColorMode&&(options.nodes.font.color="#fff"),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 e=canvas.toDataURL(MIME_TYPE);downloadButton.href=e,downloadButton.download="topology"}),graph.on("dragEnd",function(e){if(dragged=this.getPositions(e.nodes),coord_save_checkbox.checked&&0!==Object.keys(dragged).length)for(dragged_device in dragged){var t=dragged_device,n=new XMLHttpRequest;n.open("PATCH","/api/plugins/netbox_topology_views/save-coords/save_coords/"),n.setRequestHeader("X-CSRFToken",csrftoken),n.setRequestHeader("Accept","application/json"),n.setRequestHeader("Content-Type","application/json"),n.onreadystatechange=function(){4===n.readyState&&(console.log(n.status),console.log(n.responseText))};var o=JSON.stringify({node_id:t,x:dragged[t].x,y:dragged[t].y});n.send(o)}}))}
var graph=null,container=null,downloadButton=null,MIME_TYPE="image/png",canvas=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"},shadow:{enabled:!0}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[],coord_save_checkbox=null,htmlElement=null;function getCookie(e){var t=null;if(document.cookie&&""!==document.cookie)for(var o=document.cookie.split(";"),n=0;n<o.length;n++){var d=o[n].trim();if(d.substring(0,e.length+1)===e+"="){t=decodeURIComponent(d.substring(e.length+1));break}}return t}function htmlTitle(e){return(container=document.createElement("div")).innerHTML=e,container}function addEdge(e){e.title=htmlTitle(e.title),edges.add(e)}function addNode(e){e.title=htmlTitle(e.title),nodes.add(e)}function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){csrftoken=getCookie("csrftoken"),container=document.getElementById("visgraph"),htmlElement=document.getElementsByTagName("html")[0],downloadButton=document.getElementById("btnDownloadImage"),handleLoadData(),btnFullView=document.getElementById("btnFullView"),coord_save_checkbox=document.getElementById("id_save_coords")},!1)}function performGraphDownload(){var e=document.createElement("a"),t=canvas.toDataURL(MIME_TYPE);e.href=t,e.download="topology",document.body.appendChild(e),e.click(),document.body.removeChild(e)}function handleLoadData(){null!==topology_data&&("dark"==htmlElement.dataset.netboxColorMode&&(options.nodes.font.color="#fff"),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],downloadButton.onclick=function(e){return performGraphDownload(),!1},graph.on("dragEnd",function(e){if(dragged=this.getPositions(e.nodes),coord_save_checkbox.checked&&0!==Object.keys(dragged).length)for(dragged_device in dragged){var t=dragged_device,o=new XMLHttpRequest;o.open("PATCH","/api/plugins/netbox_topology_views/save-coords/save_coords/"),o.setRequestHeader("X-CSRFToken",csrftoken),o.setRequestHeader("Accept","application/json"),o.setRequestHeader("Content-Type","application/json"),o.onreadystatechange=function(){4===o.readyState&&(console.log(o.status),console.log(o.responseText))};var n=JSON.stringify({node_id:t,x:dragged[t].x,y:dragged[t].y});o.send(n)}}))}
@@ -2,3 +2,6 @@
height: 70vh;
}
html[data-netbox-color-mode=dark] #visgraph {
background-color: #212529;
}
+16 -6
View File
@@ -27,6 +27,9 @@ var options = {
font: {
face: 'helvetica',
},
shadow: {
enabled: true
}
},
physics: {
solver: 'forceAtlas2Based'
@@ -75,13 +78,24 @@ function iniPlotboxIndex() {
csrftoken = getCookie('csrftoken');
container = document.getElementById('visgraph');
htmlElement = document.getElementsByTagName("html")[0];
handleLoadData();
downloadButton = document.getElementById('btnDownloadImage');
handleLoadData();
btnFullView = document.getElementById('btnFullView');
coord_save_checkbox = document.getElementById('id_save_coords');
}, false);
}
function performGraphDownload() {
var tempDownloadLink = document.createElement('a');
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
tempDownloadLink.href = generatedImageUrl;
tempDownloadLink.download = "topology";
document.body.appendChild(tempDownloadLink);
tempDownloadLink.click();
document.body.removeChild(tempDownloadLink);
}
function handleLoadData() {
if (topology_data !== null) {
@@ -100,11 +114,7 @@ function handleLoadData() {
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";
});
downloadButton.onclick = function(e) { performGraphDownload(); return false; };
graph.on("dragEnd", function (params) {
dragged = this.getPositions(params.nodes);
@@ -16,7 +16,7 @@
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info">
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<i class="mdi mdi-download"></i>
Download
</a>
+225 -136
View File
@@ -11,12 +11,82 @@ from .filters import DeviceFilterSet
import json
from dcim.models import Device, Cable, DeviceRole, DeviceType
from dcim.models import Device, Cable, DeviceRole, PathEndpoint
from circuits.models import CircuitTermination, ProviderNetwork
from extras.models import Tag
def get_topology_data(queryset, hide_unconnected):
nodes = []
nodes_ids = []
def create_node(device):
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
node_content = ""
if device.device_type is not None:
node_content += "<tr><th>Type: </th><td>" + device.device_type.model + "</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"] is not None:
if ";" in 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
return node
def create_edge(edge_id, cable, termination_a, termination_b, path = None, circuit = None):
cable_a_dev_name = "device A name unknown" if termination_a.device.name is None else termination_a.device.name
cable_a_name = "device A name unknown" if termination_a.name is None else termination_a.name
cable_b_dev_name = "device A name unknown" if termination_b.device.name is None else termination_b.device.name
cable_b_name = "cable B name unknown" if termination_b.name is None else termination_b.name
edge = {}
edge["id"] = edge_id
edge["from"] = termination_a.device.id
edge["to"] = termination_b.device.id
if circuit is not None:
edge["dashes"] = True
edge["title"] = "Circuit provider: " + circuit.provider.name + "<br>"
edge["title"] += "Termination between <br>"
edge["title"] += cable_b_dev_name + " [" + cable_b_name + "]<br>"
edge["title"] += cable_a_dev_name + " [" + cable_a_name + "]"
else:
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
if path is not None:
edge["title"] += "" if len(path) <= 0 else "<br>Through " + "/".join(path)
if cable is not None and cable.color != "":
edge["color"] = "#" + cable.color
return edge
def get_topology_data(queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections):
nodes_devices = {}
edges = []
edge_ids = 0
cable_ids = []
@@ -25,184 +95,203 @@ def get_topology_data(queryset, hide_unconnected):
return None
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
enable_circuit_terminations = settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]
device_ids = [d.id for d in queryset]
for qs_device in queryset:
device_has_connections = False
links = Cable.objects.filter( Q(_termination_a_device_id__in=device_ids) | Q(_termination_b_device_id__in=device_ids) ) \
.select_related("termination_a_type", "termination_b_type") \
.prefetch_related("termination_a", "termination_b")
for link in links:
if link.termination_a_type.name in ignore_cable_type or link.termination_b_type.name in ignore_cable_type \
or link.id in cable_ids:
continue
a_is_path_endoint = isinstance(link.termination_a, PathEndpoint)
b_is_path_endoint = isinstance(link.termination_b, PathEndpoint)
if not end2end_connections or (not a_is_path_endoint and not b_is_path_endoint):
if isinstance(link.termination_a, CircuitTermination):
if enable_circuit_terminations and link.termination_a.circuit.id not in circuit_ids:
circuit = link.termination_a.circuit
circuit_ids.append(circuit.id)
cable_ids.append(link.id)
path_destination = circuit.termination_z if link.termination_a.term_side == "A" else circuit.termination_a
if path_destination is not None and path_destination.provider_network is None:
# ProviderNetwork not supported at the moment
# $path_destination.cable would be none : there is no cable between a CircuitTermination and ProviderNetwork
origin_device = link.termination_b.device
destination_device = path_destination.cable.termination_b.device
if origin_device.id in device_ids and destination_device.id in device_ids:
if origin_device.id not in nodes_devices:
nodes_devices[origin_device.id] = origin_device
if destination_device.id not in nodes_devices:
nodes_devices[destination_device.id] = destination_device
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"
edges.append(create_edge(edge_ids, link, link.termination_b, path_destination.cable.termination_b, [circuit.cid], circuit))
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
elif link.termination_a.device.id in device_ids and link.termination_b.device.id in device_ids:
if link.termination_a.device.id not in nodes_devices:
nodes_devices[link.termination_a.device.id] = link.termination_a.device
if link.termination_b.device.id not in nodes_devices:
nodes_devices[link.termination_b.device.id] = link.termination_b.device
cable_ids.append(link.id)
edge_ids += 1
edges.append(create_edge(edge_ids, link, link.termination_a, link.termination_b))
else:
# termination_a can be a CircuitTermination (no trace()) while termination_b is a PathEndpoint
# If so, we swap them so we can follow the path using PathEndpoint#trace()
path_start = link.termination_a if a_is_path_endoint else link.termination_b
if path_start.device is not None and path_start.device.id not in device_ids:
# device not in queryset, skip
continue
# Ignore incomplete path when in end-to-end connection mode
if path_start.path is not None:
if not path_start.path.is_active or path_start.path.is_split:
continue
else:
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
if link_from.termination_a_type.name == "circuit termination":
if link_from.termination_a.circuit.id not in circuit_ids:
circuit_ids.append(link_from.termination_a.circuit.id)
edge_ids += 1
continue
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"
path_destination = path_start.path.destination
if hasattr(path_destination, "device") and path_destination.device is not None and path_destination.device.id not in device_ids:
# device not in queryset, skip
continue
edge = {}
edge["id"] = edge_ids
edge["to"] = link_from.termination_b.device.id
edge["dashes"] = True
title = ""
valid_path = False
title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>"
title += "Termination between <br>"
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
# variables needed for trace() browsing
origin = None
circuit = None
tmp_path = []
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
# trace() : Return the path as a list of three-tuples (A termination, cable, B termination)
# TODO device qui ne s"affichent pas si hide_unconnected
for (termination_a, cable, termination_b) in path_start.trace():
if origin is None:
# New part of the link
origin = termination_a
circuit = None
tmp_path = []
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 isinstance(termination_b, ProviderNetwork):
# ProviderNetwork not supported at the moment
# It would need to manage several kind of nodes (Device, ProviderNetwork)
# and maps javascript nodes ID with Devices IDs and ProviderNetworks IDs
# When $termination_b is a ProviderNetwork instance, $cable will be None
break
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
if cable is not None and (cable.termination_a_type in ignore_cable_type or cable.termination_b_type in ignore_cable_type):
# Ignore this path
break
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 isinstance(termination_b, CircuitTermination):
if enable_circuit_terminations:
circuit = termination_b.circuit
tmp_path.append(circuit.cid)
if circuit.id not in circuit_ids:
circuit_ids.append(circuit.id)
else:
# Ignore this path
break
elif end2end_connections and termination_b != path_destination \
and termination_b.device.device_role.id not in intermediate_dev_role_ids \
and termination_b.device.id not in device_ids:
# Skip this intermediate device, origin device is keep in $origin for next iteration
tmp_path.append(termination_b.device.name)
elif cable is not None and termination_b is not None:
if cable.id not in cable_ids:
# New part of the link we want to display
cable_ids.append(cable.id)
edge_ids += 1
edges.append(create_edge(edge_ids, cable, origin, termination_b, tmp_path, circuit))
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)
if not valid_path:
valid_path = True
dev_name = qs_device.name
if dev_name is None:
dev_name = "device name unknown"
if termination_b.device.id not in nodes_devices and \
(termination_b.device.device_role.id in intermediate_dev_role_ids or termination_b.device.id in device_ids) :
nodes_devices[termination_b.device.id] = termination_b.device
# Reset for next iteration
origin = None
# endfor (trace)
node_content = ""
if valid_path and path_start.device.id not in nodes_devices:
nodes_devices[path_start.device.id] = path_start.device
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)
# endfor (link)
for qs_device in queryset:
if qs_device.id not in nodes_devices and not hide_unconnected:
nodes_devices[qs_device.id] = qs_device
results = {}
results["nodes"] = nodes
results["nodes"] = [create_node(d) for d in nodes_devices.values()]
results["edges"] = edges
return results
class TopologyHomeView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
permission_required = ("dcim.view_site", "dcim.view_device")
"""
Show the home page
"""
def get(self, request):
self.filterset = DeviceFilterSet
self.queryset = Device.objects.all()
self.queryset = Device.objects.all().select_related("device_type", "device_role")
self.queryset = self.filterset(request.GET, self.queryset).qs
topo_data = None
if request.GET:
hide_unconnected = None
if 'hide_unconnected' in request.GET:
hide_unconnected = False
if "hide_unconnected" in request.GET:
if request.GET["hide_unconnected"] == "on" :
hide_unconnected = True
if 'draw_init' in request.GET:
if request.GET["draw_init"].lower() == 'true':
topo_data = get_topology_data(self.queryset, hide_unconnected)
if "intermediate_dev_role_id" in request.GET:
intermediate_dev_role_ids = list(map(int, request.GET.getlist("intermediate_dev_role_id")))
else:
topo_data = get_topology_data(self.queryset, hide_unconnected)
intermediate_dev_role_ids = []
end2end_connections = False
if "end2end_connections" in request.GET:
if request.GET["end2end_connections"] == "on" :
end2end_connections = True
if "draw_init" in request.GET:
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
else:
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_intermediate_dev_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_intermediate_dev_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_device_role_id = DeviceRole.objects.filter(name__in=preselected_device_roles).values_list("id", flat=True)
q_intermediate_dev_role_id = DeviceRole.objects.filter(name__in=preselected_intermediate_dev_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"]
q.setlist("device_role_id", list(q_device_role_id))
q.setlist("intermediate_dev_role_id", list(q_intermediate_dev_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)
return render(request, "netbox_topology_views/index.html" , {
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data)
}
)
+28 -6096
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "1.1.0",
"version": "2.0.0",
"scripts": {
"resources": "gulp build",
"resources_dev": "gulp build_dev"
+1 -1
View File
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name='netbox-topology-views',
version='1.1.0',
version='2.0.0',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',