diff --git a/README.md b/README.md index 795069f..496efbe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ # netbox-topology-views +## preview + +![preview image](doc/img/preview.png?raw=true "preview") + ## 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 -go to /plugins/topology-views/ \ No newline at end of file +go to /plugins/topology-views/ view your topologies \ No newline at end of file diff --git a/doc/img/preview.png b/doc/img/preview.png new file mode 100644 index 0000000..83a411c Binary files /dev/null and b/doc/img/preview.png differ diff --git a/netbox_topology_views/__init__.py b/netbox_topology_views/__init__.py index 334709e..843bbdc 100644 --- a/netbox_topology_views/__init__.py +++ b/netbox_topology_views/__init__.py @@ -10,9 +10,9 @@ class TopologyViewsConfig(PluginConfig): 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', + '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' + '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 diff --git a/netbox_topology_views/api/__init__.py b/netbox_topology_views/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py index 5dc8463..76f571d 100644 --- a/netbox_topology_views/api/serializers.py +++ b/netbox_topology_views/api/serializers.py @@ -1,10 +1,10 @@ -from rest_framework.serializers import ModelSerializer, ValidatedModelSerializer +from rest_framework.serializers import ModelSerializer from dcim.models import DeviceRole -class PreDeviceRoleSerializer(ValidatedModelSerializer): +class PreDeviceRoleSerializer(ModelSerializer): class Meta: model = DeviceRole - fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description') + fields = ('id', 'name') \ No newline at end of file diff --git a/netbox_topology_views/api/urls.py b/netbox_topology_views/api/urls.py index 21fe127..7fac369 100644 --- a/netbox_topology_views/api/urls.py +++ b/netbox_topology_views/api/urls.py @@ -1,12 +1,9 @@ from rest_framework import routers -from .views import PreSelectDeviceRolesViewSet +from .views import PreSelectDeviceRolesViewSet, SearchViewSet 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('search', SearchViewSet, basename='search') urlpatterns = router.urls \ No newline at end of file diff --git a/netbox_topology_views/api/views.py b/netbox_topology_views/api/views.py index 0e57673..8aed3ba 100644 --- a/netbox_topology_views/api/views.py +++ b/netbox_topology_views/api/views.py @@ -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 django.conf import settings -from dcim.models import DeviceRole +from utilities.api import IsAuthenticatedOrLoginNotRequired -class PreSelectDeviceRolesViewSet(ModelViewSet): - queryset = DeviceRole.objects.all() +from dcim.models import DeviceRole, Device, Cable + + +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] + + 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
" + cable.termination_a.device.name + " [" + cable.termination_a.name + "]
" + 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) \ No newline at end of file diff --git a/netbox_topology_views/static/netbox_topology_views/js/app.js b/netbox_topology_views/static/netbox_topology_views/js/app.js index 67d5d83..c5aef68 100644 --- a/netbox_topology_views/static/netbox_topology_views/js/app.js +++ b/netbox_topology_views/static/netbox_topology_views/js/app.js @@ -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('Loading data'),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('Drawing network'),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('Ready')}),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('Failed to update coordinates')},success:function(e){$("#coordstatus").html('Updated coordinates'),$.ajax({url:"/api/reload_devices",type:"GET",headers:{"X-CSRFToken":csrftoken},success:function(e){}})}}))})})},error:function(e){$("#status").html('Something went wrong')}})})} \ No newline at end of file +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('Loading data'),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('Drawing network'),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('Ready')}),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('Failed to update coordinates')},success:function(e){$("#coordstatus").html('Updated coordinates'),$.ajax({url:"/api/reload_devices",type:"GET",headers:{"X-CSRFToken":csrftoken},success:function(e){}})}}))})})},error:function(e){$("#status").html('Something went wrong')}})})} \ No newline at end of file diff --git a/netbox_topology_views/static_dev/js/home.js b/netbox_topology_views/static_dev/js/home.js index ecabb37..83f4d6a 100644 --- a/netbox_topology_views/static_dev/js/home.js +++ b/netbox_topology_views/static_dev/js/home.js @@ -11,7 +11,7 @@ var options = { }, nodes: { shape: 'image', - brokenImage: '/static/assets/img/role-unknown.png', + brokenImage: '/static/netbox_topology_views/img/role-unknown.png', size: 35, font: { multi: 'md', @@ -46,7 +46,7 @@ function startLoadSearchBar() { theme: "bootstrap", multiple: true, ajax: { - url: "/api/device_roles", + url: "/api/dcim/device-roles/?brief=true", dataType: "json", type: "GET", data: function (params) { @@ -57,7 +57,7 @@ function startLoadSearchBar() { }, processResults: function (data) { return { - results: $.map(data, function (item) { + results: $.map(data.results, function (item) { return { text: item.name, id: item.id @@ -73,7 +73,7 @@ function startLoadSearchBar() { theme: "bootstrap", multiple: true, ajax: { - url: "/api/sites", + url: "/api/dcim/sites/?brief=true", dataType: "json", type: "GET", data: function (params) { @@ -85,7 +85,7 @@ function startLoadSearchBar() { }, processResults: function (data) { return { - results: $.map(data, function (item) { + results: $.map(data.results, function (item) { return { text: item.name, id: item.id @@ -99,9 +99,9 @@ function startLoadSearchBar() { var deviceRolesSelect = $('#device-roles'); $.ajax({ type: 'GET', - url: '/api/preselect_device_roles' + url: '/api/plugins/topology-views/preselectdeviceroles/' }).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); deviceRolesSelect.append(option).trigger('change'); deviceRolesSelect.trigger({ @@ -123,7 +123,7 @@ function handleButtonPress() { var value3 = $("#sites").val(); $.ajax({ type: "POST", - url: "/api/search", + url: "/api/plugins/topology-views/search/search/", data: JSON.stringify({ 'name': value, 'devicerole': value2, diff --git a/netbox_topology_views/templates/netbox_topology_views/index.html b/netbox_topology_views/templates/netbox_topology_views/index.html index 02a52c1..79e91e8 100644 --- a/netbox_topology_views/templates/netbox_topology_views/index.html +++ b/netbox_topology_views/templates/netbox_topology_views/index.html @@ -62,6 +62,7 @@ +{% endwith %} {% endblock %} {% block javascript %} diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 27b9e00..bf1c275 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -1,7 +1,6 @@ from django.shortcuts import get_object_or_404, render from django.views.generic import View - class TopologyHomeView(View): """ Show the home page