fixed auth, added save-coords

This commit is contained in:
Mattijs Vanhaverbeke
2020-05-14 22:20:01 +02:00
parent a1d155fdbc
commit 5e53066ba7
12 changed files with 99 additions and 41 deletions
+7 -1
View File
@@ -18,8 +18,11 @@ Then run `python3 manage.py collectstatic --no-input`
### Custom field: coordinates
There is also support for custom fiels.
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
The coordinates can then be provided as: "X;Y"
## Configure
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
@@ -37,7 +40,10 @@ PLUGINS_CONFIG = {
| 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. |
| 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 | 'poweroutlet,powerport' | The cable types that you want to ignore in the views |
### Custom Images
+4 -3
View File
@@ -4,15 +4,16 @@ class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render toplogoy maps'
version = '0.3'
version = '0.4.0'
author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com'
base_url = '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': 'dcim.poweroutlet,dcim.powerport',
'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'
'ignore_cable_type': 'poweroutlet,powerport',
'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
}
config = TopologyViewsConfig
+8 -1
View File
@@ -1,10 +1,17 @@
from rest_framework.serializers import ModelSerializer
from dcim.models import DeviceRole
from dcim.models import DeviceRole, Device
class PreDeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ('id', 'name')
class TopologyDummySerializer(ModelSerializer):
class Meta:
model = Device
fields = ('id', 'name')
+2 -1
View File
@@ -1,9 +1,10 @@
from rest_framework import routers
from .views import PreSelectDeviceRolesViewSet, SearchViewSet
from .views import PreSelectDeviceRolesViewSet, SearchViewSet, SaveCoordsViewSet
router = routers.DefaultRouter()
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
router.register('search', SearchViewSet, basename='search')
router.register('save-coords', SaveCoordsViewSet, basename='save_coords')
urlpatterns = router.urls
+61 -15
View File
@@ -1,24 +1,68 @@
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet, GenericViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import PreDeviceRoleSerializer
from .serializers import PreDeviceRoleSerializer, TopologyDummySerializer
from django.conf import settings
from utilities.api import IsAuthenticatedOrLoginNotRequired
from dcim.models import DeviceRole, Device, Cable
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(",")
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_device_roles = preselected_device_roles_raw.split(",")
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
serializer_class = PreDeviceRoleSerializer
class SearchViewSet(ViewSet):
_ignore_model_permissions = True
permission_classes = [IsAuthenticatedOrLoginNotRequired]
class SaveCoordsViewSet(GenericViewSet):
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
@action(detail=False, methods=['post'])
def save_coords(self, request):
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
device_id = None
x_coord = None
y_coord = None
if "node_id" in request.data:
if request.data["node_id"]:
device_id = request.data["node_id"]
if "x" in request.data:
if request.data["x"]:
x_coord = request.data["x"]
if "y" in request.data:
if request.data["y"]:
y_coord = request.data["y"]
actual_device= Device.objects.get(id=device_id)
for device_custom_field in actual_device.custom_field_values.all():
if device_custom_field.field.name == "coordinates":
old_cords = device_custom_field.serialized_value.split(";")
device_custom_field.value = "%s;%s" % (x_coord,y_coord)
device_custom_field.save()
actual_device.save()
results = {}
results["status"] = "ok"
return Response(results)
print('notok')
#TODO
return Response(status=500)
else:
return Response(status=500)
class SearchViewSet(GenericViewSet):
#_ignore_model_permissions = True
#permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
def _filter(self, site, role, name):
filter_devices = Device.objects.all()
@@ -47,6 +91,7 @@ class SearchViewSet(ViewSet):
name = request.data["name"]
devices = self._filter(sites, devicerole, name)
nodes = []
edges = []
edge_ids = 0
@@ -55,14 +100,15 @@ class SearchViewSet(ViewSet):
cables = device.get_cables()
for cable in cables:
if cable.id not in cable_ids:
cable_ids.append(cable.id)
edge_ids += 1
edge = {}
edge["id"] = edge_ids
edge["from"] = cable.termination_a.device.id
edge["to"] = cable.termination_b.device.id
edge["title"] = "Connection between <br> " + cable.termination_a.device.name + " [" + cable.termination_a.name + "]<br>" + cable.termination_b.device.name + " [" + cable.termination_b.name + "]"
edges.append(edge)
if cable.termination_a_type.name not in ignore_cable_type:
cable_ids.append(cable.id)
edge_ids += 1
edge = {}
edge["id"] = edge_ids
edge["from"] = cable.termination_a.device.id
edge["to"] = cable.termination_b.device.id
edge["title"] = "Connection between <br> " + cable.termination_a.device.name + " [" + cable.termination_a.name + "]<br>" + cable.termination_b.device.name + " [" + cable.termination_b.name + "]"
edges.append(edge)
node = {}
node["id"] = device.id
node["name"] = device.name
@@ -1 +1 @@
var graph=null,container=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!0},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"}};function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("visgraph"),csrftoken=jQuery("[name=csrfmiddlewaretoken]").val(),startLoadSearchBar(),handleButtonPress()},!1)}function startLoadSearchBar(){$("#device-roles").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"/api/dcim/device-roles/?brief=true",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}}),$("#sites").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"/api/dcim/sites/?brief=true",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}});var n=$("#device-roles");$.ajax({type:"GET",url:"/api/plugins/topology-views/preselectdeviceroles/"}).then(function(e){$.each(e.results,function(e,t){var a=new Option(t.name,t.id,!0,!0);n.append(a).trigger("change"),n.trigger({type:"select2:select",params:{data:t}})})})}function handleButtonPress(){$("#search-form").submit(function(e){$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>'),e.preventDefault();var t=$("#name").val(),a=$("#device-roles").val(),n=$("#sites").val();$.ajax({type:"POST",url:"/api/plugins/topology-views/search/search/",data:JSON.stringify({name:t,devicerole:a,sites:n}),headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){$("#status").html('<span class="badge badge-pill badge-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(e.nodes,function(e,t){nodes.add(t)}),$.each(e.edges,function(e,t){edges.add(t)}),graph.fit(),graph.on("afterDrawing",function(){$("#status").html('<span class="badge badge-pill badge-success">Ready</span>')}),graph.on("dragEnd",function(e){dragged=this.getPositions(e.nodes),$.each(dragged,function(e,t){$("#coordstatus").html(""),$("#checkSaveCoordinates").is(":checked")&&(nodes.update({id:e,physics:!1}),$.ajax({url:"/api/save_coords",type:"POST",dataType:"json",headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",data:JSON.stringify({node_id:e,x:t.x,y:t.y}),error:function(e){$("#coordstatus").html('<span class="badge badge-pill badge-warning">Failed to update coordinates</span>')},success:function(e){$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>'),$.ajax({url:"/api/reload_devices",type:"GET",headers:{"X-CSRFToken":csrftoken},success:function(e){}})}}))})})},error:function(e){$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>')}})})}
var graph=null,container=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!0},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"}};function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("visgraph"),csrftoken=jQuery("[name=csrfmiddlewaretoken]").val(),startLoadSearchBar(),handleButtonPress()},!1)}function startLoadSearchBar(){$("#device-roles").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"../../api/dcim/device-roles/?brief=true",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}}),$("#sites").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"../../api/dcim/sites/?brief=true",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}});var n=$("#device-roles");$.ajax({type:"GET",url:"../../api/plugins/topology-views/preselectdeviceroles/"}).then(function(e){$.each(e.results,function(e,t){var a=new Option(t.name,t.id,!0,!0);n.append(a).trigger("change"),n.trigger({type:"select2:select",params:{data:t}})})})}function handleButtonPress(){$("#search-form").submit(function(e){$("#status").html('<span class="label label-info">Loading data</span>'),e.preventDefault();var t=$("#name").val(),a=$("#device-roles").val(),n=$("#sites").val();$.ajax({type:"POST",url:"../../api/plugins/topology-views/search/search/",data:JSON.stringify({name:t,devicerole:a,sites:n}),headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){$("#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(e.nodes,function(e,t){nodes.add(t)}),$.each(e.edges,function(e,t){edges.add(t)}),graph.fit(),graph.on("afterDrawing",function(){$("#status").html('<span class="label label-success">Ready</span>')}),graph.on("dragEnd",function(e){dragged=this.getPositions(e.nodes),$.each(dragged,function(e,t){$("#coordstatus").html(""),$("#checkSaveCoordinates").is(":checked")&&(nodes.update({id:e,physics:!1}),$.ajax({url:"../../api/plugins/topology-views/save-coords/save_coords/",type:"POST",dataType:"json",headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",data:JSON.stringify({node_id:e,x:t.x,y:t.y}),error:function(e){$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>')},success:function(e){$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>')}}))})})},error:function(e){$("#status").html('<span class="label label-warning">Something went wrong</span>')}})})}
File diff suppressed because one or more lines are too long
+7 -15
View File
@@ -116,7 +116,7 @@ function startLoadSearchBar() {
function handleButtonPress() {
$("#search-form").submit(function (event) {
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
$("#status").html('<span class="label label-info">Loading data</span>');
event.preventDefault();
var value = $("#name").val();
var value2 = $("#device-roles").val();
@@ -133,7 +133,7 @@ function handleButtonPress() {
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data_result) {
$("#status").html('<span class="badge badge-pill badge-info">Drawing network</span>');
$("#status").html('<span class="label label-info">Drawing network</span>');
graph = null;
nodes = new vis.DataSet();
edges = new vis.DataSet();
@@ -147,7 +147,7 @@ function handleButtonPress() {
graph.fit();
graph.on('afterDrawing', function () {
$("#status").html('<span class="badge badge-pill badge-success">Ready</span>');
$("#status").html('<span class="label label-success">Ready</span>');
});
graph.on("dragEnd", function (params) {
@@ -157,7 +157,7 @@ function handleButtonPress() {
if ($('#checkSaveCoordinates').is(":checked")) {
nodes.update({ id: node_id, physics: false });
$.ajax({
url: "../../api/save_coords",
url: "../../api/plugins/topology-views/save-coords/save_coords/",
type: 'POST',
dataType: 'json',
headers: { "X-CSRFToken": csrftoken },
@@ -168,18 +168,10 @@ function handleButtonPress() {
'y': coordinates.y
}),
error: function (error_result) {
$("#coordstatus").html('<span class="badge badge-pill badge-warning">Failed to update coordinates</span>');
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
},
success: function (data_result) {
$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>');
$.ajax({
url: '/api/reload_devices',
type: 'GET',
headers: { "X-CSRFToken": csrftoken },
success: function (data) {
//nothing
},
});
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
},
});
}
@@ -187,7 +179,7 @@ function handleButtonPress() {
});
},
error: function (error_result) {
$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>');
$("#status").html('<span class="label label-warning">Something went wrong</span>');
},
});
});
@@ -45,6 +45,7 @@
</div>
{% if config.allow_coordinates_saving == True %}
<div class="panel panel-default mt-3">
<div class="panel-heading">
<strong>Settings</strong>
@@ -59,6 +60,7 @@
</div>
</div>
</div>
{% endif %}
</div>
</div>
+4 -1
View File
@@ -1,7 +1,10 @@
from django.shortcuts import get_object_or_404, render
from django.views.generic import View
from django.contrib.auth.mixins import PermissionRequiredMixin
class TopologyHomeView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
class TopologyHomeView(View):
"""
Show the home page
"""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "0.2",
"version": "0.4.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='0.3',
version='0.4.0',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',