Compare commits
10
Commits
v0.5.1
...
v1.0.0-alpha.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deedee78bd | ||
|
|
683e125cd5 | ||
|
|
3b4209223e | ||
|
|
6fc7cce79a | ||
|
|
fd4a1cd0c7 | ||
|
|
2deaed1adc | ||
|
|
55a61d4d10 | ||
|
|
3a4652543f | ||
|
|
9b4dd77943 | ||
|
|
91a690f1c8 |
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '16 22 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -35,6 +35,7 @@ Then run `python3 manage.py collectstatic --no-input`
|
||||
|
||||
| netbox version | netbox-topology-views version |
|
||||
| ------------- |-------------|
|
||||
| >= 2.11.1 | >= v0.5.3 |
|
||||
| >= 2.10.0 | >= v0.5.0 |
|
||||
| < 2.10.0 | =< v0.4.10 |
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ class TopologyViewsConfig(PluginConfig):
|
||||
name = 'netbox_topology_views'
|
||||
verbose_name = 'Topology views'
|
||||
description = 'An plugin to render topology maps'
|
||||
version = '0.5.1'
|
||||
version = '1.0.0-alpha.1'
|
||||
author = 'Mattijs Vanhaverbeke'
|
||||
author_email = 'author@example.com'
|
||||
base_url = 'topology-views'
|
||||
base_url = 'netbox_topology_views'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from rest_framework import routers
|
||||
from . import views
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = views.TopologyViewsRootView
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
|
||||
router.register('preselecttags', views.PreSelectTagsViewSet)
|
||||
|
||||
@@ -78,7 +78,7 @@ class SearchViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
def _filter(self, site, role, name, tags, region):
|
||||
def _filter(self, site, role, name, tags, region, location):
|
||||
filter_devices = Device.objects.all()
|
||||
if name is not None:
|
||||
filter_devices = filter_devices.filter(name__contains=name)
|
||||
@@ -90,6 +90,8 @@ class SearchViewSet(ReadOnlyModelViewSet):
|
||||
filter_devices = filter_devices.filter(tags__id__in=tags)
|
||||
if region is not None:
|
||||
filter_devices = filter_devices.filter(site__region__id__in=region)
|
||||
if location is not None:
|
||||
filter_devices = filter_devices.filter(location__id__in=location)
|
||||
return filter_devices
|
||||
|
||||
|
||||
@@ -115,14 +117,15 @@ class SearchViewSet(ReadOnlyModelViewSet):
|
||||
if regions == []:
|
||||
regions = None
|
||||
|
||||
locations = request.query_params.getlist('locations[]', None)
|
||||
if locations == []:
|
||||
locations = None
|
||||
|
||||
hide_unconnected = request.query_params.get('hide_unconnected', None)
|
||||
if hide_unconnected == "":
|
||||
hide_unconnected = None
|
||||
print(hide_unconnected)
|
||||
if hide_unconnected == 'false':
|
||||
print("ok")
|
||||
|
||||
devices = self._filter(sites, devicerole, name, tags, regions)
|
||||
devices = self._filter(sites, devicerole, name, tags, regions, locations)
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import django_filters
|
||||
from extras.filters import TagFilter
|
||||
from dcim.models import Device, DeviceRole, Region, Site, Location
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from django.db.models import Q
|
||||
|
||||
class DeviceFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_role_id',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Location (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', ]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -0,0 +1,49 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.models import Tag
|
||||
from dcim.models import Device, Site, Region, DeviceRole, Location
|
||||
from extras.forms import CustomFieldModelFilterForm
|
||||
|
||||
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, BootstrapMixin)
|
||||
|
||||
class DeviceFilterForm(CustomFieldModelFilterForm):
|
||||
model = Device
|
||||
field_groups = [
|
||||
['q'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['device_role_id'],
|
||||
['tag'],
|
||||
]
|
||||
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
device_role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Device Role')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Location')
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -5,6 +5,13 @@ menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_topology_views:home',
|
||||
link_text='Topology',
|
||||
buttons=()
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:home',
|
||||
title='Topology View',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=[],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
@@ -33,283 +33,42 @@ var options = {
|
||||
}
|
||||
};
|
||||
var selected_regions = [];
|
||||
var selected_sites = [];
|
||||
|
||||
function addEdge(item) {
|
||||
edges.add(item);
|
||||
}
|
||||
|
||||
function addNode(item) {
|
||||
nodes.add(item);
|
||||
}
|
||||
|
||||
function iniPlotboxIndex() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
container = document.getElementById('visgraph');
|
||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
||||
startLoadSearchBar();
|
||||
handleButtonPress();
|
||||
handleLoadData();
|
||||
downloadButton = document.getElementById('btnDownloadImage');
|
||||
btnFullView = document.getElementById('btnFullView');
|
||||
}, 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 = {
|
||||
q: 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: 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 = "®ion_id=" + selected_regions[i];
|
||||
base_url = base_url + tmp;
|
||||
}
|
||||
return base_url;
|
||||
}
|
||||
function handleLoadData() {
|
||||
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#tags').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/extras/tags/?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').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
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
graph = null;
|
||||
nodes = new vis.DataSet();
|
||||
edges = new vis.DataSet();
|
||||
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||
|
||||
topology_data.edges.forEach(addEdge);
|
||||
topology_data.nodes.forEach(addNode);
|
||||
|
||||
$('#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 = [];
|
||||
});
|
||||
graph.fit();
|
||||
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
graph.on('afterDrawing', function () {
|
||||
var image = canvas.toDataURL(MIME_TYPE);
|
||||
downloadButton.href = image;
|
||||
downloadButton.download = "topology";
|
||||
});
|
||||
});
|
||||
|
||||
var tagsSelect = $('#tags');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '../../api/plugins/topology-views/preselecttags/'
|
||||
}).then(function (data) {
|
||||
$.each(data.results, function (index, tags_to_preload) {
|
||||
var option = new Option(tags_to_preload.name, tags_to_preload.id, true, true);
|
||||
tagsSelect.append(option).trigger('change');
|
||||
tagsSelect.trigger({
|
||||
type: 'select2:select',
|
||||
params: {
|
||||
data: tags_to_preload
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonPress() {
|
||||
$("#search-form").submit(function (event) {
|
||||
$("#status").html('<span class="label label-info">Loading data</span>');
|
||||
event.preventDefault();
|
||||
var value = $("#name").val();
|
||||
var value2 = $("#device-roles").val();
|
||||
var value3 = $("#sites").val();
|
||||
var value4 = $("#tags").val();
|
||||
var value5 = $("#regions").val();
|
||||
var value6 = $('#checkHideUnconnected').is(":checked");
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "../../api/plugins/topology-views/search/search/",
|
||||
data: {
|
||||
'name': value,
|
||||
'devicerole[]': value2,
|
||||
'sites[]': value3,
|
||||
'tags[]': value4,
|
||||
'regions[]': value5,
|
||||
'hide_unconnected': value6
|
||||
},
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
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();
|
||||
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('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) {
|
||||
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/plugins/topology-views/save-coords/save_coords/",
|
||||
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,
|
||||
'y': coordinates.y
|
||||
}),
|
||||
error: function (error_result) {
|
||||
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
|
||||
},
|
||||
success: function (data_result) {
|
||||
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
error: function (error_result) {
|
||||
$("#status").html('<span class="label label-warning">Something went wrong</span>');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,99 +1,78 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load perms %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
{% block title %}Topology Views{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
<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 class="buttons pull-right">
|
||||
<a id="btnDownloadImage" class="btn btn-xs btn-info">
|
||||
<i class="mdi mdi-download"></i>
|
||||
</a>
|
||||
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
|
||||
<i class="mdi mdi-share"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
<a id="btnDownloadImage" class="btn btn-sm btn-info">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<a id="btnFullView" class="btn btn-sm btn-info disabled" target="_blank">
|
||||
<i class="mdi mdi-share"></i>
|
||||
Share
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
|
||||
Network
|
||||
</button>
|
||||
</li>
|
||||
{% if filter_form %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
Filters
|
||||
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock tab_items %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||
<div class="panel-body">
|
||||
<div id="visgraph" class=""></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var topology_data = {{ topology_data | safe }};
|
||||
</script>
|
||||
</div>
|
||||
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</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" autocomplete="off">
|
||||
{% 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>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="checkHideUnconnected">
|
||||
<label class="custom-control-label" for="checkHideUnconnected">Hide Unconnected</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mb-4">
|
||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if config.allow_coordinates_saving == True %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{% 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 %}
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
@@ -7,5 +7,4 @@ 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'),
|
||||
)
|
||||
|
||||
+157
-10
@@ -1,6 +1,151 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.db.models import Q
|
||||
from django.views.generic import View
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.conf import settings
|
||||
|
||||
from .forms import DeviceFilterForm
|
||||
from .filters import DeviceFilterSet
|
||||
import json
|
||||
|
||||
from dcim.models import Device, Cable, DeviceRole, DeviceType
|
||||
|
||||
def get_topology_data(queryset):
|
||||
nodes = []
|
||||
nodes_ids = []
|
||||
edges = []
|
||||
edge_ids = 0
|
||||
cable_ids = []
|
||||
circuit_ids = []
|
||||
if not queryset:
|
||||
return None
|
||||
|
||||
for qs_device in queryset:
|
||||
device_has_connections = False
|
||||
links_from_device = Cable.objects.filter(Q(_termination_a_device_id=qs_device.id) | Q(_termination_b_device_id=qs_device.id) )
|
||||
for link_from in links_from_device:
|
||||
device_has_connections = True
|
||||
if link_from.termination_a_type.name != "circuit termination" and link_from.termination_b_type.name != "circuit termination":
|
||||
if link_from.id not in cable_ids:
|
||||
cable_ids.append(link_from.id)
|
||||
edge_ids += 1
|
||||
cable_a_dev_name = link_from.termination_a.device.name
|
||||
if cable_a_dev_name is None:
|
||||
cable_a_dev_name = "device A name unknown"
|
||||
cable_a_name = link_from.termination_a.name
|
||||
if cable_a_name is None:
|
||||
cable_a_name = "cable A name unknown"
|
||||
cable_b_dev_name = link_from.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = link_from.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["from"] = link_from.termination_a.device.id
|
||||
edge["to"] = link_from.termination_b.device.id
|
||||
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||
if link_from.color != "":
|
||||
edge["color"] = "#" + link_from.color
|
||||
edges.append(edge)
|
||||
else:
|
||||
if link_from.termination_a.circuit.id not in circuit_ids:
|
||||
circuit_ids.append(link_from.termination_a.circuit.id)
|
||||
edge_ids += 1
|
||||
|
||||
cable_b_dev_name = link_from.termination_b.device.name
|
||||
if cable_b_dev_name is None:
|
||||
cable_b_dev_name = "device B name unknown"
|
||||
cable_b_name = link_from.termination_b.name
|
||||
if cable_b_name is None:
|
||||
cable_b_name = "cable B name unknown"
|
||||
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["to"] = link_from.termination_b.device.id
|
||||
edge["dashes"] = True
|
||||
title = ""
|
||||
|
||||
title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>"
|
||||
title += "Termination between <br>"
|
||||
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
|
||||
|
||||
if link_from.termination_a.circuit.termination_a is not None and link_from.termination_a.circuit.termination_a.cable is not None and link_from.termination_a.circuit.termination_a.cable.id != link_from.id and link_from.termination_a.circuit.termination_a.cable.termination_b is not None and link_from.termination_a.circuit.termination_a.cable.termination_b.device is not None:
|
||||
edge["from"] = link_from.termination_a.circuit.termination_a.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = link_from.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 = link_from.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)
|
||||
|
||||
if link_from.termination_a.circuit.termination_z is not None and link_from.termination_a.circuit.termination_z.cable is not None and link_from.termination_a.circuit.termination_z.cable.id != link_from.id and link_from.termination_a.circuit.termination_z.cable.termination_b is not None and link_from.termination_a.circuit.termination_z.cable.termination_b.device is not None:
|
||||
edge["from"] = link_from.termination_a.circuit.termination_z.cable.termination_b.device.id
|
||||
|
||||
cable_a_dev_name = link_from.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 = link_from.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)
|
||||
|
||||
if qs_device.id not in nodes_ids:
|
||||
nodes_ids.append(qs_device.id)
|
||||
|
||||
dev_name = qs_device.name
|
||||
if dev_name is None:
|
||||
dev_name = "device name unknown"
|
||||
|
||||
node_content = ""
|
||||
|
||||
if qs_device.device_type is not None:
|
||||
node_content += "<tr><th>Type: </th><td>" + qs_device.device_type.model + "</td></tr>"
|
||||
if qs_device.device_role.name is not None:
|
||||
node_content += "<tr><th>Role: </th><td>" + qs_device.device_role.name + "</td></tr>"
|
||||
if qs_device.serial != "":
|
||||
node_content += "<tr><th>Serial: </th><td>" + qs_device.serial + "</td></tr>"
|
||||
if qs_device.primary_ip is not None:
|
||||
node_content += "<tr><th>IP Address: </th><td>" + str(qs_device.primary_ip.address) + "</td></tr>"
|
||||
|
||||
dev_title = "<table> %s </table>" % (node_content)
|
||||
|
||||
node = {}
|
||||
node["id"] = qs_device.id
|
||||
node["name"] = dev_name
|
||||
node["label"] = dev_name
|
||||
node["title"] = dev_title
|
||||
node["shape"] = 'image'
|
||||
if qs_device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
|
||||
node["image"] = '../../static/netbox_topology_views/img/' + qs_device.device_role.slug + ".png"
|
||||
else:
|
||||
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
|
||||
|
||||
if qs_device.device_role.color != "":
|
||||
node["color.border"] = "#" + qs_device.device_role.color
|
||||
|
||||
if "coordinates" in qs_device.custom_field_data:
|
||||
print(qs_device.custom_field_data)
|
||||
if qs_device.custom_field_data["coordinates"] is not None:
|
||||
cords = qs_device.custom_field_data["coordinates"].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 results
|
||||
|
||||
|
||||
class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
permission_required = ('dcim.view_site', 'dcim.view_device')
|
||||
@@ -8,14 +153,16 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
"""
|
||||
Show the home page
|
||||
"""
|
||||
def get(self, request):
|
||||
return render(request, 'netbox_topology_views/index.html')
|
||||
def get(self, request):
|
||||
self.filterset = DeviceFilterSet
|
||||
self.queryset = Device.objects.all()
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
topo_data = None
|
||||
if request.GET:
|
||||
topo_data = get_topology_data(self.queryset)
|
||||
|
||||
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')
|
||||
return render(request, 'netbox_topology_views/index.html' , {
|
||||
'filter_form': DeviceFilterForm(request.GET, label_suffix=''),
|
||||
'topology_data': json.dumps(topo_data)
|
||||
}
|
||||
)
|
||||
|
||||
Generated
+12
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "netbox_topology_views",
|
||||
"version": "0.5.1",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -754,12 +754,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
@@ -2328,9 +2322,9 @@
|
||||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"http-signature": {
|
||||
@@ -2771,9 +2765,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"loud-rejection": {
|
||||
@@ -4805,12 +4799,13 @@
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
|
||||
"integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz",
|
||||
"integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^3.0.0"
|
||||
"camelcase": "^3.0.0",
|
||||
"object.assign": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "0.5.1",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"scripts": {
|
||||
"resources": "gulp build",
|
||||
"resources_dev": "gulp build_dev"
|
||||
|
||||
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='netbox-topology-views',
|
||||
version='0.5.1',
|
||||
version='1.0.0-alpha.1',
|
||||
description='An NetBox plugin to create Topology maps',
|
||||
url='https://github.com/mattieserver/netbox-topology-views',
|
||||
author='Mattijs Vanhaverbeke',
|
||||
|
||||
Reference in New Issue
Block a user