added base functions and api
This commit is contained in:
@@ -1,8 +1,17 @@
|
|||||||
# netbox-topology-views
|
# netbox-topology-views
|
||||||
|
|
||||||
|
## preview
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
|
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file.
|
||||||
|
Then run python3 manage.py collectstatic --no-input
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## use
|
## use
|
||||||
|
|
||||||
go to /plugins/topology-views/
|
go to /plugins/topology-views/ view your topologies
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -10,9 +10,9 @@ class TopologyViewsConfig(PluginConfig):
|
|||||||
base_url = 'topology-views'
|
base_url = 'topology-views'
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'PRESELECTED_DEVICE_ROLES': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
'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',
|
'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'
|
'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'
|
||||||
}
|
}
|
||||||
|
|
||||||
config = TopologyViewsConfig
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from rest_framework.serializers import ModelSerializer, ValidatedModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
from dcim.models import DeviceRole
|
from dcim.models import DeviceRole
|
||||||
|
|
||||||
class PreDeviceRoleSerializer(ValidatedModelSerializer):
|
class PreDeviceRoleSerializer(ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
|
fields = ('id', 'name')
|
||||||
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .views import PreSelectDeviceRolesViewSet
|
from .views import PreSelectDeviceRolesViewSet, SearchViewSet
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
#router.register('device_roles', AnimalViewSet)
|
|
||||||
#router.register('sites', AnimalViewSet)
|
|
||||||
#router.register('search', AnimalViewSet)
|
|
||||||
#router.register('save_coords', AnimalViewSet)
|
|
||||||
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
|
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
|
||||||
|
router.register('search', SearchViewSet, basename='search')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
@@ -1,9 +1,89 @@
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .serializers import PreDeviceRoleSerializer
|
from .serializers import PreDeviceRoleSerializer
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from dcim.models import DeviceRole
|
from utilities.api import IsAuthenticatedOrLoginNotRequired
|
||||||
|
|
||||||
class PreSelectDeviceRolesViewSet(ModelViewSet):
|
from dcim.models import DeviceRole, Device, Cable
|
||||||
queryset = DeviceRole.objects.all()
|
|
||||||
|
|
||||||
|
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
|
serializer_class = PreDeviceRoleSerializer
|
||||||
|
|
||||||
|
class SearchViewSet(ViewSet):
|
||||||
|
_ignore_model_permissions = True
|
||||||
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
|
||||||
|
def _filter(self, site, role, name):
|
||||||
|
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)
|
||||||
|
return filter_devices
|
||||||
|
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'])
|
||||||
|
def search(self, request):
|
||||||
|
name = None
|
||||||
|
sites = None
|
||||||
|
devicerole = None
|
||||||
|
if "devicerole" in request.data:
|
||||||
|
if request.data["devicerole"]:
|
||||||
|
devicerole = request.data["devicerole"]
|
||||||
|
if "sites" in request.data:
|
||||||
|
if request.data["sites"]:
|
||||||
|
sites = request.data["sites"]
|
||||||
|
if "name" in request.data:
|
||||||
|
if request.data["name"]:
|
||||||
|
name = request.data["name"]
|
||||||
|
|
||||||
|
devices = self._filter(sites, devicerole, name)
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
edge_ids = 0
|
||||||
|
cable_ids = []
|
||||||
|
for device in devices:
|
||||||
|
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)
|
||||||
|
node = {}
|
||||||
|
node["id"] = device.id
|
||||||
|
node["name"] = device.name
|
||||||
|
node["label"] = device.name + device.device_type.display_name
|
||||||
|
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"
|
||||||
|
|
||||||
|
for device_custom_field in device.custom_field_values.all():
|
||||||
|
if device_custom_field.field.name == "coordinates":
|
||||||
|
cords = device_custom_field.serialized_value.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)
|
||||||
@@ -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/assets/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/device_roles",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e,function(e){return{text:e.name,id:e.id}})}}}}),$("#sites").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"/api/sites",dataType:"json",type:"GET",data:function(e){return{term:e.term}},processResults:function(e){return{results:$.map(e,function(e){return{text:e.name,id:e.id}})}}}});var n=$("#device-roles");$.ajax({type:"GET",url:"/api/preselect_device_roles"}).then(function(e){$.each(e.data,function(e,a){var t=new Option(a.name,a.id,!0,!0);n.append(t).trigger("change"),n.trigger({type:"select2:select",params:{data:a}})})})}function handleButtonPress(){$("#search-form").submit(function(e){$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>'),e.preventDefault();var a=$("#name").val(),t=$("#device-roles").val(),n=$("#sites").val();$.ajax({type:"POST",url:"/api/search",data:JSON.stringify({name:a,devicerole:t,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,a){nodes.add(a)}),$.each(e.edges,function(e,a){edges.add(a)}),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,a){$("#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:a.x,y:a.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="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>')}})})}
|
||||||
@@ -11,7 +11,7 @@ var options = {
|
|||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
shape: 'image',
|
shape: 'image',
|
||||||
brokenImage: '/static/assets/img/role-unknown.png',
|
brokenImage: '/static/netbox_topology_views/img/role-unknown.png',
|
||||||
size: 35,
|
size: 35,
|
||||||
font: {
|
font: {
|
||||||
multi: 'md',
|
multi: 'md',
|
||||||
@@ -46,7 +46,7 @@ function startLoadSearchBar() {
|
|||||||
theme: "bootstrap",
|
theme: "bootstrap",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
ajax: {
|
ajax: {
|
||||||
url: "/api/device_roles",
|
url: "/api/dcim/device-roles/?brief=true",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
type: "GET",
|
type: "GET",
|
||||||
data: function (params) {
|
data: function (params) {
|
||||||
@@ -57,7 +57,7 @@ function startLoadSearchBar() {
|
|||||||
},
|
},
|
||||||
processResults: function (data) {
|
processResults: function (data) {
|
||||||
return {
|
return {
|
||||||
results: $.map(data, function (item) {
|
results: $.map(data.results, function (item) {
|
||||||
return {
|
return {
|
||||||
text: item.name,
|
text: item.name,
|
||||||
id: item.id
|
id: item.id
|
||||||
@@ -73,7 +73,7 @@ function startLoadSearchBar() {
|
|||||||
theme: "bootstrap",
|
theme: "bootstrap",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
ajax: {
|
ajax: {
|
||||||
url: "/api/sites",
|
url: "/api/dcim/sites/?brief=true",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
type: "GET",
|
type: "GET",
|
||||||
data: function (params) {
|
data: function (params) {
|
||||||
@@ -85,7 +85,7 @@ function startLoadSearchBar() {
|
|||||||
},
|
},
|
||||||
processResults: function (data) {
|
processResults: function (data) {
|
||||||
return {
|
return {
|
||||||
results: $.map(data, function (item) {
|
results: $.map(data.results, function (item) {
|
||||||
return {
|
return {
|
||||||
text: item.name,
|
text: item.name,
|
||||||
id: item.id
|
id: item.id
|
||||||
@@ -99,9 +99,9 @@ function startLoadSearchBar() {
|
|||||||
var deviceRolesSelect = $('#device-roles');
|
var deviceRolesSelect = $('#device-roles');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
url: '/api/preselect_device_roles'
|
url: '/api/plugins/topology-views/preselectdeviceroles/'
|
||||||
}).then(function (data) {
|
}).then(function (data) {
|
||||||
$.each(data.data, function (index, device_role_to_preload) {
|
$.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);
|
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
||||||
deviceRolesSelect.append(option).trigger('change');
|
deviceRolesSelect.append(option).trigger('change');
|
||||||
deviceRolesSelect.trigger({
|
deviceRolesSelect.trigger({
|
||||||
@@ -123,7 +123,7 @@ function handleButtonPress() {
|
|||||||
var value3 = $("#sites").val();
|
var value3 = $("#sites").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: "/api/search",
|
url: "/api/plugins/topology-views/search/search/",
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
'name': value,
|
'name': value,
|
||||||
'devicerole': value2,
|
'devicerole': value2,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
|
|
||||||
class TopologyHomeView(View):
|
class TopologyHomeView(View):
|
||||||
"""
|
"""
|
||||||
Show the home page
|
Show the home page
|
||||||
|
|||||||
Reference in New Issue
Block a user