Compare commits

...
21 Commits
Author SHA1 Message Date
mattieserver 0340522f59 update jquery version to match netbox v2.9.1 (#43)
* Update full.html

* Update setup.py

* Update package.json

* Update __init__.py
2020-08-23 11:53:52 +02:00
Mattijs Vanhaverbeke 2edd597871 bump version 2020-07-20 15:49:06 +02:00
mattieserver 5f4f97c360 Merge pull request #41 from mattieserver/32
added Filter topology via URL Parameter
2020-07-20 15:00:10 +02:00
Mattijs Vanhaverbeke 17ae8b4a1d #32 2020-07-20 14:57:09 +02:00
mattieserver bb9765e011 Merge pull request #40 from mattieserver/37
add filter by region
2020-07-20 12:38:15 +02:00
Mattijs Vanhaverbeke 5bfc47775b #37 2020-07-20 12:36:38 +02:00
mattieserver 216508170f Merge pull request #39 from mattieserver/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-20 11:07:20 +02:00
dependabot[bot] 0faa135978 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 07:57:54 +00:00
mattieserver 9ba61cce28 Merge pull request #28 from mattieserver/22052020_dev
22052020 dev
2020-05-24 18:31:07 +02:00
mattieserver 9824a3f659 Merge branch 'master' into 22052020_dev 2020-05-24 18:30:59 +02:00
mattieserver 4115584306 Update README.md 2020-05-23 18:20:04 +02:00
mattieserver cd239ac6ae Update README.md 2020-05-23 18:19:13 +02:00
mattieserver 030844f2fb Merge pull request #27 from ryanmerolle/rm-issue-26
Update README to include local_requirements setup
2020-05-23 18:15:56 +02:00
ryanmerolle 2e6dd7de23 Update README to include local_requirements setup 2020-05-23 12:04:19 -04:00
Mattijs Vanhaverbeke de5707ff06 PATCH instead of POST 2020-05-22 12:57:58 +02:00
Mattijs Vanhaverbeke 371c5c3502 added download button, fixed perms 2020-05-22 12:07:46 +02:00
mattieserver 26139cbb1c Merge pull request #24 from mattieserver/19052020_dev_1
19052020 dev 1
2020-05-20 08:11:04 +02:00
Mattijs Vanhaverbeke 4ea315d4b2 v0.4.8 2020-05-20 08:09:45 +02:00
Mattijs Vanhaverbeke 40ae715eb5 more NoneType checks 2020-05-19 22:35:55 +02:00
Mattijs Vanhaverbeke 204098a143 #23 check for nonetype in cable.termination 2020-05-19 22:17:13 +02:00
mattieserver 9b606643ea Update README.md 2020-05-19 19:50:56 +02:00
14 changed files with 960 additions and 274 deletions
+26 -2
View File
@@ -1,7 +1,8 @@
# Netbox Topology Views Plugin
Create topology views/maps from your devices in netbox.
Support to filter on name, site and device role.
The connections are based on the cables you created in netbox.
Support to filter on name, site, tag and device role.
## Preview
@@ -9,9 +10,22 @@ Support to filter on name, site and device role.
## Install
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env.
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file.
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
```no-highlight
# echo netbox-topology-views >> local_requirements.txt
```
Once installed, the plugin needs to be enabled in your `configuration.py`
```python
# In your configuration.py
PLUGINS = ["netbox_topology_views"]
```
Then run `python3 manage.py collectstatic --no-input`
@@ -62,4 +76,14 @@ Run `pip install netbox-topology-views --upgrade` in your venv.
Run `python3 manage.py collectstatic --no-input`
Clear you browser cache.
### Permissions
To view `/plugins/topology-views/` you need the following permissions:
+ dcim | device | can view device
+ dcim | site | can view site
+ extras | tag | can view tag
+ dcim | device role | can view device role
+1 -1
View File
@@ -4,7 +4,7 @@ class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render toplogoy maps'
version = '0.4.7'
version = '0.4.10'
author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com'
base_url = 'topology-views'
+53 -42
View File
@@ -30,11 +30,11 @@ class PreSelectTagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.filter(name__in=preselected_tags)
serializer_class = PreTagSerializer
class SaveCoordsViewSet(GenericViewSet):
class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
@action(detail=False, methods=['post'])
@action(detail=False, methods=['patch'])
def save_coords(self, request):
results = {}
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
@@ -76,14 +76,14 @@ class SaveCoordsViewSet(GenericViewSet):
results["status"] = "not allowed to save coords"
return Response(results, status=500)
class SearchViewSet(GenericViewSet):
class SearchViewSet(ReadOnlyModelViewSet):
#_ignore_model_permissions = True
#permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
def _filter(self, site, role, name, tags):
def _filter(self, site, role, name, tags, region):
filter_devices = Device.objects.all()
if name is not None:
filter_devices = filter_devices.filter(name__contains=name)
@@ -93,29 +93,34 @@ class SearchViewSet(GenericViewSet):
filter_devices = filter_devices.filter(device_role__id__in=role)
if tags is not None:
filter_devices = filter_devices.filter(tags__id__in=tags)
if region is not None:
filter_devices = filter_devices.filter(site__region__id__in=region)
return filter_devices
@action(detail=False, methods=['post'])
@action(detail=False, methods=['get'])
def search(self, request):
name = None
sites = None
devicerole = None
tags = 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"]
if "tags" in request.data:
if request.data["tags"]:
tags = request.data["tags"]
name = request.query_params.get('name', None)
if name == "":
name = None
devices = self._filter(sites, devicerole, name, tags)
sites = request.query_params.getlist('sites[]', None)
if sites == []:
sites = None
devicerole = request.query_params.getlist('devicerole[]', None)
if devicerole == []:
devicerole = None
tags = request.query_params.getlist('tags[]', None)
if tags == []:
tags = None
regions = request.query_params.getlist('regions[]', None)
if regions == []:
regions = None
devices = self._filter(sites, devicerole, name, tags, regions)
nodes = []
edges = []
@@ -175,34 +180,40 @@ class SearchViewSet(GenericViewSet):
title += "Circuit provider: " + cable.termination_a.circuit.provider.name + "<br>"
title += "Termination between <br>"
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
# To Many if's
if cable.termination_a.circuit.termination_a is not None:
if cable.termination_a.circuit.termination_a.cable is not None:
if cable.termination_a.circuit.termination_a.cable.id != cable.id:
edge["from"] = cable.termination_a.circuit.termination_a.cable.termination_b.device.id
if cable.termination_a.circuit.termination_a.cable.termination_b is not None:
if cable.termination_a.circuit.termination_a.cable.termination_b.device is not None:
edge["from"] = cable.termination_a.circuit.termination_a.cable.termination_b.device.id
cable_a_dev_name = cable.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 = cable.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>"
cable_a_dev_name = cable.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 = cable.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)
# To Many if's
if cable.termination_a.circuit.termination_z is not None:
if cable.termination_a.circuit.termination_z.cable is not None:
if cable.termination_a.circuit.termination_z.cable.id != cable.id:
edge["from"] = cable.termination_a.circuit.termination_z.cable.termination_b.device.id
if cable.termination_a.circuit.termination_z.cable.termination_b is not None:
if cable.termination_a.circuit.termination_z.cable.termination_b.device is not None:
edge["from"] = cable.termination_a.circuit.termination_z.cable.termination_b.device.id
cable_a_dev_name = cable.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 = cable.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)
cable_a_dev_name = cable.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 = cable.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)
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
var graph = null;
var container = null;
function iniPlotboxFull() {
document.addEventListener('DOMContentLoaded', function () {
container = document.getElementById('fullvisgraph');
startRender();
}, false);
}
function startRender() {
var url = location.search;
$.ajax({
type: "GET",
url: "../../api/plugins/topology-views/search/search/" + url,
contentType: "application/json; charset=utf-8",
success: function (data_result, status, xhr) {
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();
canvas = document.getElementById('fullvisgraph').getElementsByTagName('canvas')[0];
}
});
}
+91 -10
View File
@@ -1,5 +1,8 @@
var graph = null;
var container = null;
var downloadButton = null;
var MIME_TYPE = "image/png";
var canvas = null;
var csrftoken = null;
var nodes = new vis.DataSet();
var edges = new vis.DataSet();
@@ -29,6 +32,7 @@ var options = {
solver: 'forceAtlas2Based'
}
};
var selected_regions = [];
function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () {
@@ -36,6 +40,8 @@ function iniPlotboxIndex() {
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
startLoadSearchBar();
handleButtonPress();
downloadButton = document.getElementById('btnDownloadImage');
btnFullView = document.getElementById('btnFullView');
}, false);
}
@@ -73,11 +79,23 @@ function startLoadSearchBar() {
theme: "bootstrap",
multiple: true,
ajax: {
url: "../../api/dcim/sites/?brief=true",
url: function (params) {
var base_url = "../../api/dcim/sites/?brief=true";
if (selected_regions.length == 0) {
return base_url;
}
else {
for (var i = 0; i < selected_regions.length; i++) {
var tmp = "&region_id=" + selected_regions[i];
base_url = base_url + tmp;
}
return base_url;
}
},
dataType: "json",
type: "GET",
data: function (params) {
var queryParameters = {
q: params.term
}
@@ -122,6 +140,53 @@ function startLoadSearchBar() {
}
}
});
$('#regions').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
multiple: true,
ajax: {
url: "../../api/dcim/regions/?brief=true",
dataType: "json",
type: "GET",
data: function (params) {
var queryParameters = {
q: params.term
}
return queryParameters;
},
processResults: function (data) {
return {
results: $.map(data.results, function (item) {
return {
text: item.name,
id: item.id
}
})
};
}
}
});
$('#regions').on('select2:select', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
if (index == -1) {
selected_regions.push(data.id.toString());
}
});
$('#regions').on('select2:unselect', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
console.log(index);
if (index > -1) {
selected_regions.splice(index, 1);
}
});
$('#regions').on('select2:clear', function (e) {
selected_regions = [];
});
var deviceRolesSelect = $('#device-roles');
$.ajax({
@@ -166,19 +231,29 @@ function handleButtonPress() {
var value2 = $("#device-roles").val();
var value3 = $("#sites").val();
var value4 = $("#tags").val();
var value5 = $("#regions").val();
$.ajax({
type: "POST",
type: "GET",
url: "../../api/plugins/topology-views/search/search/",
data: JSON.stringify({
data: {
'name': value,
'devicerole': value2,
'sites': value3,
'tags': value4
}),
'devicerole[]': value2,
'sites[]': value3,
'tags[]': value4,
'regions[]': value5
},
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data_result) {
beforeSend: function(){
new_url = this.url.split("../../api/plugins/topology-views/search/search/?");
new_url = new_url[1];
new_url = "../../plugins/topology-views/full?" + new_url
btnFullView.classList.remove("disabled");
btnFullView.href = new_url;
},
success: function (data_result, status, xhr) {
$("#status").html('<span class="label label-info">Drawing network</span>');
graph = null;
nodes = new vis.DataSet();
@@ -191,9 +266,13 @@ function handleButtonPress() {
edges.add(edge);
});
graph.fit();
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
graph.on('afterDrawing', function () {
$("#status").html('<span class="label label-success">Ready</span>');
var image = canvas.toDataURL(MIME_TYPE);
downloadButton.href = image;
downloadButton.download = "topology";
});
graph.on("dragEnd", function (params) {
@@ -204,10 +283,11 @@ function handleButtonPress() {
//nodes.update({ id: node_id, physics: false });
$.ajax({
url: "../../api/plugins/topology-views/save-coords/save_coords/",
type: 'POST',
type: 'PATCH',
dataType: 'json',
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
processData: false,
data: JSON.stringify({
'node_id': node_id,
'x': coordinates.x,
@@ -223,6 +303,7 @@ function handleButtonPress() {
}
});
});
},
error: function (error_result) {
$("#status").html('<span class="label label-warning">Something went wrong</span>');
@@ -0,0 +1,24 @@
{% 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="panel-body">
<div id="fullvisgraph" class=""></div>
</div>
{% endwith %}
{% endblock %}
{% block javascript %}
<script src="{% static 'jquery/jquery-3.5.1.min.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
<script type="application/javascript"> iniPlotboxFull()</script>
{% endblock %}
@@ -12,6 +12,14 @@
<div class="panel panel-default">
<div class="panel-heading">
<strong>Network</strong> <span id="status"></span>
<div class="buttons pull-right">
<a id="btnDownloadImage" class="btn btn-xs btn-info">
<i class="fa fa-download"></i>
</a>
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
<i class="fa fa-share"></i>
</a>
</div>
</div>
<div class="panel-body">
<div id="visgraph" class=""></div>
@@ -41,6 +49,10 @@
<div class="form-group">
<label for="tags">Tags</label>
<select class="form-control" multiple="multiple" id="tags"></select>
</div>
<div class="form-group">
<label for="regions">Regions</label>
<select class="form-control" multiple="multiple" id="regions"></select>
</div>
<hr class="mb-4">
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
+1
View File
@@ -7,4 +7,5 @@ from . import views
# a specific view so that it can be accessed by users.
urlpatterns = (
path('', views.TopologyHomeView.as_view(), name='home'),
path('full', views.TopologyFullView.as_view(), name='full'),
)
+10 -1
View File
@@ -9,4 +9,13 @@ class TopologyHomeView(PermissionRequiredMixin, View):
Show the home page
"""
def get(self, request):
return render(request, 'netbox_topology_views/index.html')
return render(request, 'netbox_topology_views/index.html')
class TopologyFullView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
"""
Show the full view page
"""
def get(self, request):
return render(request, 'netbox_topology_views/full.html')
+692 -211
View File
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "0.4.7",
"version": "0.4.10",
"scripts": {
"resources": "gulp build",
"resources_dev": "gulp build_dev"
@@ -9,10 +9,20 @@
"dependencies": {},
"devDependencies": {
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-sass": "^4.0.2",
"gulp-sass": "^4.1.0",
"gulp-uglify": "^3.0.2",
"vis-network": "^7.6.4"
"vis-network": "^7.10.0",
"keycharm": "^0.3.0",
"moment": "^2.24.0",
"timsort": "^0.3.0",
"vis-data": "^6.2.1",
"vis-util": "^4.0.0",
"uuid": "7.0.0",
"@egjs/hammerjs" : "^2.0.0"
},
"peerDependencies": {
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name='netbox-topology-views',
version='0.4.7',
version='0.4.10',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',