Merge pull request #2 from mattieserver/dev

initial functions
This commit is contained in:
mattieserver
2020-04-16 14:26:57 +02:00
committed by GitHub
37 changed files with 5185 additions and 1 deletions
+2
View File
@@ -127,3 +127,5 @@ dmypy.json
# Pyre type checker
.pyre/
node_modules
+17 -1
View File
@@ -1 +1,17 @@
# netbox-topology-views
# 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/ view your topologies
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+78
View File
@@ -0,0 +1,78 @@
var gulp = require('gulp');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var cleanCss = require('gulp-clean-css');
var jsVendorFiles = [
'node_modules/vis-network/standalone/umd/vis-network.min.js',
];
var cssVendorFiles = [
'node_modules/vis-network/styles/vis-network.min.css',
];
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
var jsLocalResourceFiles = './netbox_topology_views/static_dev/js/*.js',
jsDest = './netbox_topology_views/static/netbox_topology_views/js/';
function styles_local() {
return gulp.src(sassFiles)
.pipe(sass().on('error', sass.logError))
.pipe(concat('app.css'))
.pipe(cleanCss())
.pipe(gulp.dest(cssDest));
}
exports.styles_local = styles_local
function styles_vendor() {
return gulp.src(cssVendorFiles)
.pipe(concat('vendor.css'))
.pipe(cleanCss())
.pipe(gulp.dest(cssDest))
}
exports.styles_vendor = styles_vendor
function js_local() {
return gulp.src(jsLocalResourceFiles)
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest(jsDest))
}
exports.js_local = js_local
function js_vendor() {
return gulp.src(jsVendorFiles)
.pipe(concat('vendor.js'))
.pipe(uglify())
.pipe(gulp.dest(jsDest))
}
exports.js_vendor = js_vendor
function js_local_dev() {
return gulp.src(jsLocalResourceFiles)
.pipe(concat('app.js'))
.pipe(gulp.dest(jsDest))
}
exports.js_local_dev = js_local_dev
function js_vendor_dev() {
return gulp.src(jsVendorFiles)
.pipe(concat('vendor.js'))
.pipe(gulp.dest(jsDest))
}
exports.js_vendor_dev = js_vendor_dev
exports.css = gulp.series(styles_local, styles_vendor);
exports.js = gulp.series(js_local, js_vendor);
exports.js_dev = gulp.series(js_local_dev, js_vendor_dev);
exports.build = gulp.series(exports.css, exports.js);
exports.build_dev = gulp.series(exports.css, exports.js_dev);
+18
View File
@@ -0,0 +1,18 @@
from extras.plugins import PluginConfig
class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render toplogoy maps'
version = '0.1'
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'
}
config = TopologyViewsConfig
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
#no models yet
+10
View File
@@ -0,0 +1,10 @@
from rest_framework.serializers import ModelSerializer
from dcim.models import DeviceRole
class PreDeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ('id', 'name')
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import routers
from .views import PreSelectDeviceRolesViewSet, SearchViewSet
router = routers.DefaultRouter()
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
router.register('search', SearchViewSet, basename='search')
urlpatterns = router.urls
+89
View File
@@ -0,0 +1,89 @@
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 utilities.api import IsAuthenticatedOrLoginNotRequired
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 <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)
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# no modeles yet
@@ -0,0 +1 @@
#visgraph{height:70vh}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

@@ -0,0 +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>')}})})}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
#visgraph {
height: 70vh;
}
+194
View File
@@ -0,0 +1,194 @@
var graph = null;
var container = null;
var csrftoken = null;
var nodes = new vis.DataSet();
var edges = new vis.DataSet();
var options = {
interaction: {
hover: true,
hoverConnectedEdges: true,
multiselect: true
},
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();
}, 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 = {
term: 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: "/api/dcim/sites/?brief=true",
dataType: "json",
type: "GET",
data: function (params) {
var queryParameters = {
term: 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
}
});
});
});
}
function handleButtonPress() {
$("#search-form").submit(function (event) {
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
event.preventDefault();
var value = $("#name").val();
var value2 = $("#device-roles").val();
var value3 = $("#sites").val();
$.ajax({
type: "POST",
url: "/api/plugins/topology-views/search/search/",
data: JSON.stringify({
'name': value,
'devicerole': value2,
'sites': value3
}),
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data_result) {
$("#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(data_result["nodes"], function (index, device) {
nodes.add(device);
});
$.each(data_result["edges"], function (index, edge) {
edges.add(edge);
});
graph.fit();
graph.on('afterDrawing', function () {
$("#status").html('<span class="badge badge-pill badge-success">Ready</span>');
});
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/save_coords",
type: 'POST',
dataType: 'json',
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
'node_id': node_id,
'x': coordinates.x,
'y': coordinates.y
}),
error: function (error_result) {
$("#coordstatus").html('<span class="badge badge-pill badge-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
},
});
},
});
}
});
});
},
error: function (error_result) {
$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>');
},
});
});
}
@@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% 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="row ">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Network</strong> <span id="status"></span>
</div>
<div class="panel-body">
<div id="visgraph" class=""></div>
</div>
</div>
</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">
{% 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="sites">Sites</label>
<select class="form-control" multiple="multiple" id="sites"></select>
</div>
<hr class="mb-4">
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
</form>
</div>
</div>
<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>
</div>
</div>
{% 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 %}
+10
View File
@@ -0,0 +1,10 @@
from django.urls import path
from . import views
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
# a specific view so that it can be accessed by users.
urlpatterns = (
path('', views.TopologyHomeView.as_view(), name='home'),
)
+9
View File
@@ -0,0 +1,9 @@
from django.shortcuts import get_object_or_404, render
from django.views.generic import View
class TopologyHomeView(View):
"""
Show the home page
"""
def get(self, request):
return render(request, 'netbox_topology_views/index.html')
+4627
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "0.0.1",
"scripts": {
"resources": "gulp build",
"resources_dev": "gulp build_dev"
},
"dependencies": {},
"devDependencies": {
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.1",
"gulp-sass": "^4.0.2",
"gulp-uglify": "^3.0.2",
"vis-data": "^6.2.3",
"vis-util": "^1.1.8",
"vis-network": "^6.4.4"
}
}
+14
View File
@@ -0,0 +1,14 @@
from setuptools import setup, find_packages
setup(
name='netbox-topology-views',
version='0.1',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',
license='Apache 2.0',
install_requires=[],
packages=find_packages(),
include_package_data=True,
)