Compare commits

..
Author SHA1 Message Date
Mattijs Vanhaverbeke 962e219325 fixed coords check 2022-02-24 10:21:55 +01:00
Mattijs Vanhaverbeke 2387e5d0cc removed distutils 2022-02-24 10:05:37 +01:00
Mattijs Vanhaverbeke 0bcbd35082 fixed old settings using a string instead of array 2022-02-24 09:59:04 +01:00
Mattijs Vanhaverbeke 0ce1c6df13 added ignore_cable_type 2022-02-22 18:16:40 +01:00
Mattijs Vanhaverbeke 7dcd72a3f9 added filter for unconnected devices 2022-02-22 18:07:58 +01:00
Mattijs Vanhaverbeke 616c0ab1b5 fixed enable_circuit_terminations 2022-02-22 16:44:32 +01:00
Mattijs Vanhaverbeke 8385dc23f8 fixed edge/node labels 2022-02-22 16:12:15 +01:00
Mattijs Vanhaverbeke 449fdf3f80 upgraded packages 2022-02-22 15:35:07 +01:00
Mattijs Vanhaverbeke b53ceed2ae added draw_default_layout 2022-02-22 15:07:30 +01:00
Mattijs Vanhaverbeke 064751f727 fix filter + added preselected roles 2022-02-22 13:54:35 +01:00
Mattijs Vanhaverbeke deedee78bd added coords + bump version 2022-02-21 18:14:39 +01:00
Mattijs Vanhaverbeke 683e125cd5 init for v3.1 2022-02-21 17:41:14 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3b4209223e Bump lodash from 4.17.20 to 4.17.21 (#66)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 22:13:26 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6fc7cce79a Bump hosted-git-info from 2.8.8 to 2.8.9 (#67)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 22:13:11 +02:00
mattieserver fd4a1cd0c7 Create codeql-analysis.yml 2021-05-07 16:20:55 +02:00
mattieserver 2deaed1adc Update README.md 2021-05-03 18:53:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 55a61d4d10 Bump ini from 1.3.5 to 1.3.8 (#47)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 18:12:42 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3a4652543f Bump yargs-parser from 5.0.0 to 5.0.1 (#62)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/v5.0.1/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/compare/v5.0.0...v5.0.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-02 22:33:51 +02:00
mattieserver 9b4dd77943 update version 2021-05-01 19:30:18 +02:00
mattieserver 91a690f1c8 add filter by locations (#63)
* frontend for locations

* locations filter backend
2021-05-01 19:22:28 +02:00
mattieserver 5f498f9ce2 Hide unconnected (#61)
* #57 hide unconnected devices
2021-05-01 18:34:50 +02:00
mattieserver 195ac47542 added docs to setup dev (#60) 2021-05-01 17:05:32 +02:00
julianze a25be917ee Update __init__.py (#55)
fix typo toplogoy
2021-03-28 11:40:03 +02:00
mattieserver 079361b190 Update README.md 2021-02-17 11:02:58 +01:00
mattieserverandmattieserver a314d2b307 netbox v2.10 (#52)
* small changes

* removed print

* fix coords

* fix saving coords

* update readme

* fixed icons

* bump version

Co-authored-by: mattieserver <mattijs@DESKTOP-618OSU1.localdomain>
2021-02-11 19:22:41 +01:00
26 changed files with 7005 additions and 798 deletions
+71
View File
@@ -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
+19 -7
View File
@@ -1,5 +1,7 @@
# Netbox Topology Views Plugin
![Version](https://img.shields.io/pypi/v/netbox-topology-views) ![Downloads](https://img.shields.io/pypi/dm/netbox-topology-views)
Create topology views/maps from your devices in netbox.
The connections are based on the cables you created in netbox.
Support to filter on name, site, tag and device role.
@@ -29,9 +31,18 @@ PLUGINS = ["netbox_topology_views"]
Then run `python3 manage.py collectstatic --no-input`
### Versions
| netbox version | netbox-topology-views version |
| ------------- |-------------|
| >= 3.1.8 | >= v1.0.0a1 |
| >= 2.11.1 | >= v0.5.3 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
### Custom field: coordinates
There is also support for custom fiels.
There is also support for custom fields.
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
@@ -45,20 +56,21 @@ Example:
```
PLUGINS_CONFIG = {
'netbox_topology_views': {
'device_img': 'router,switch,firewall',
'preselected_device_roles': 'Router, Firewall'
'device_img': ['router','switch', 'firewall'],
'preselected_device_roles': ['Router', 'Firewall']
}
}
```
| Setting | Default value | Description |
| ------------- |-------------| -----|
| 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' | The slug of the device roles that you have a image for. |
| preselected_device_roles | 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP' | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
| 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'] | The slug of the device roles that you have a image for. |
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
| ignore_cable_type | 'power outlet,power port' | The cable types that you want to ignore in the views |
| preselected_tags | '' | The name of tags you want to preload |
| ignore_cable_type | ['power outlet', 'power port'] | The cable types that you want to ignore in the views |
| preselected_tags | '[]' | The name of tags you want to preload |
| enable_circuit_terminations | False | (bool) Set to true if you want to see circuit terminations in the topology |
| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) |
### Custom Images
+22
View File
@@ -0,0 +1,22 @@
# Setup Dev
+ install docker
+ install docker compose
+ install npm
+ clone netbox repo
+ clone netbox_topology_views repo
+ create venv: `/usr/bin/python3 -m venv venv`
+ activate venv: `source venv/bin/activate`
+ install deps: `pip3 install -r requirements.txt`
+ install build tools: `python3 -m pip install --upgrade build setuptools wheel`
# Build
+ build the package: `python3 setup.py develop`
+ build the resources `npm run resources`
# Test
+ copy configuration.py from dev_setup to the netbox/netbox/ folder
+ run in the netbox repo: `python3 netbox/manage.py migrate`
+ (first time only) run in the netbox repo: `python3 netbox/manage.py createsuperuser`
+ run in the netbox repo: `python3 netbox/manage.py runserver`
+ browse to http://127.0.0.1:8000
+40
View File
@@ -0,0 +1,40 @@
###################################################################
# This file serves as a base configuration for testing purposes #
# only. It is not intended for production use. #
###################################################################
ALLOWED_HOSTS = ['*']
DATABASE = {
'NAME': 'netbox',
'USER': 'netbox',
'PASSWORD': 'J5brHrAXFLQSif0K',
'HOST': 'localhost',
'PORT': '',
'CONN_MAX_AGE': 300,
}
DEBUG = True
PLUGINS = [
'netbox_topology_views',
]
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': 't4Ph722qJ5QHeQ1qfu36',
'DATABASE': 0,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6378,
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
}
}
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+41
View File
@@ -0,0 +1,41 @@
version: '3.4'
services:
postgres:
image: postgres:12-alpine
environment:
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
POSTGRES_DB: netbox
POSTGRES_USER: netbox
ports:
- "5432:5432"
volumes:
- netbox-postgres-data:/var/lib/postgresql/data
redis:
image: redis:6-alpine
command:
- sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
environment:
REDIS_PASSWORD: t4Ph722qJ5QHeQ1qfu36 #just for local dev
volumes:
- netbox-redis-data:/data
ports:
- "6379:6379"
redis-cache:
image: redis:6-alpine
command:
- sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
environment:
REDIS_PASSWORD: H733Kdjndks81 #just for local dev
ports:
- "6378:6379"
volumes:
netbox-postgres-data:
driver: local
netbox-redis-data:
driver: local
+9 -8
View File
@@ -3,19 +3,20 @@ 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.4.10'
description = 'An plugin to render topology maps'
version = '1.0.0-alpha.6'
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',
'ignore_cable_type': 'power outlet,power port',
'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',
'preselected_device_roles': ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'],
'ignore_cable_type': ['power outlet','power port'],
'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'],
'allow_coordinates_saving': False,
'preselected_tags' : '',
'enable_circuit_terminations': False
'preselected_tags' : [],
'enable_circuit_terminations': True,
'draw_default_layout': False
}
config = TopologyViewsConfig
+1
View File
@@ -0,0 +1 @@
"""REST API"""
-12
View File
@@ -3,18 +3,6 @@ from rest_framework.serializers import ModelSerializer
from dcim.models import DeviceRole, Device
from extras.models import Tag
class PreDeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ('id', 'name')
class PreTagSerializer(ModelSerializer):
class Meta:
model = Tag
fields = ('id', 'name')
class TopologyDummySerializer(ModelSerializer):
+3 -6
View File
@@ -1,11 +1,8 @@
from rest_framework import routers
from .views import PreSelectDeviceRolesViewSet, SearchViewSet, SaveCoordsViewSet, PreSelectTagsViewSet
from . import views
router = routers.DefaultRouter()
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
router.register('preselecttags', PreSelectTagsViewSet)
router.register('search', SearchViewSet, basename='search')
router.register('save-coords', SaveCoordsViewSet, basename='save_coords')
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
urlpatterns = router.urls
+20 -221
View File
@@ -2,33 +2,20 @@ from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet,
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.contenttypes.models import ContentType
from rest_framework.routers import APIRootView
from .serializers import PreDeviceRoleSerializer, TopologyDummySerializer, PreTagSerializer
from .serializers import TopologyDummySerializer
from django.conf import settings
from utilities.api import IsAuthenticatedOrLoginNotRequired
from dcim.models import DeviceRole, Device, Cable
from circuits.models import Circuit
from extras.models import Tag, CustomField, CustomFieldValue
ignore_cable_type_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
ignore_cable_type = ignore_cable_type_raw.split(",")
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_device_roles = preselected_device_roles_raw.split(",")
preselected_tags_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
preselected_tags = preselected_tags_raw.split(",")
from extras.models import Tag
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
serializer_class = PreDeviceRoleSerializer
class TopologyViewsRootView(APIRootView):
def get_view_name(self):
return 'TopologyViews'
class PreSelectTagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.filter(name__in=preselected_tags)
serializer_class = PreTagSerializer
class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all()
@@ -38,14 +25,6 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
def save_coords(self, request):
results = {}
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
try:
cfCoords = CustomField.objects.get(name='coordinates')
except CustomField.DoesNotExist:
results["status"] = "coords custom field not created"
return Response(status=500)
obj_type = ContentType.objects.get_for_model(Device)
device_id = None
x_coord = None
y_coord = None
@@ -60,202 +39,22 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
y_coord = request.data["y"]
actual_device= Device.objects.get(id=device_id)
try:
cfvCoords = CustomFieldValue.objects.get(obj_type=obj_type, obj_id=actual_device.pk, field__name='coordinates')
cfvCoords.value = "%s;%s" % (x_coord,y_coord)
cfvCoords.save()
results["status"] = "coords set"
except CustomFieldValue.DoesNotExist:
cfvCoords = CustomFieldValue(field=cfCoords, obj_type=obj_type, obj_id=actual_device.id)
cfvCoords.value = "%s;%s" % (x_coord,y_coord)
cfvCoords.save()
results["status"] = "coords set for first time"
if "coordinates" in actual_device.custom_field_data:
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
actual_device.save()
results["status"] = "saved coords"
else:
try:
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
actual_device.save()
results["status"] = "saved coords"
except :
results["status"] = "coords custom field not created"
return Response(status=500)
return Response(results)
else:
results["status"] = "not allowed to save coords"
return Response(results, status=500)
class SearchViewSet(ReadOnlyModelViewSet):
#_ignore_model_permissions = True
#permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
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)
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)
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=['get'])
def search(self, request):
name = request.query_params.get('name', None)
if name == "":
name = None
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 = []
edge_ids = 0
cable_ids = []
circuit_ids = []
for device in devices:
cables = device.get_cables()
for cable in cables:
if cable.termination_a_type.name != "circuit termination" and cable.termination_b_type.name != "circuit termination":
if cable.id not in cable_ids:
if cable.termination_a_type.name not in ignore_cable_type and cable.termination_b_type.name not in ignore_cable_type:
cable_ids.append(cable.id)
edge_ids += 1
cable_a_dev_name = cable.termination_a.device.name
if cable_a_dev_name is None:
cable_a_dev_name = "device A name unknown"
cable_a_name = cable.termination_a.name
if cable_a_name is None:
cable_a_name = "cable A name unknown"
cable_b_dev_name = cable.termination_b.device.name
if cable_b_dev_name is None:
cable_b_dev_name = "device B name unknown"
cable_b_name = cable.termination_b.name
if cable_b_name is None:
cable_b_name = "cable B name unknown"
edge = {}
edge["id"] = edge_ids
edge["from"] = cable.termination_a.device.id
edge["to"] = cable.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 cable.color != "":
edge["color"] = "#" + cable.color
edges.append(edge)
else:
if cable.termination_a_type.name == "circuit termination":
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
if cable.termination_a.circuit.id not in circuit_ids:
circuit_ids.append(cable.termination_a.circuit.id)
edge_ids += 1
cable_b_dev_name = cable.termination_b.device.name
if cable_b_dev_name is None:
cable_b_dev_name = "device B name unknown"
cable_b_name = cable.termination_b.name
if cable_b_name is None:
cable_b_name = "cable B name unknown"
edge = {}
edge["id"] = edge_ids
edge["to"] = cable.termination_b.device.id
edge["dashes"] = True
title = ""
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:
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>"
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:
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)
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
node_content = ""
if device.device_type.display_name is not None:
node_content += "<tr><th>Type: </th><td>" + device.device_type.display_name + "</td></tr>"
if device.device_role.name is not None:
node_content += "<tr><th>Role: </th><td>" + device.device_role.name + "</td></tr>"
if device.serial != "":
node_content += "<tr><th>Serial: </th><td>" + device.serial + "</td></tr>"
if device.primary_ip is not None:
node_content += "<tr><th>IP Address: </th><td>" + str(device.primary_ip.address) + "</td></tr>"
dev_title = "<table> %s </table>" % (node_content)
node = {}
node["id"] = device.id
node["name"] = dev_name
node["label"] = dev_name
node["title"] = dev_title
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"
if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color
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)
+47
View File
@@ -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()
device_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)
+53
View File
@@ -0,0 +1,53 @@
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 utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, FilterForm)
class DeviceFilterForm(FilterForm):
model = Device
field_groups = [
['q', 'hide_unconnected'],
['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')
)
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"),
required=False,
initial=False)
tag = TagFilterField(model)
+8 -1
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
#visgraph {
height: 70vh;
}
@@ -1,33 +0,0 @@
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];
}
});
}
+32 -263
View File
@@ -33,281 +33,50 @@ var options = {
}
};
var selected_regions = [];
var selected_sites = [];
function htmlTitle(html) {
container = document.createElement("div");
container.innerHTML = html;
return container;
}
function addEdge(item) {
item.title = htmlTitle( item.title );
edges.add(item);
}
function addNode(item) {
item.title = htmlTitle( item.title );
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 = "&region_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();
$.ajax({
type: "GET",
url: "../../api/plugins/topology-views/search/search/",
data: {
'name': value,
'devicerole[]': value2,
'sites[]': value3,
'tags[]': value4,
'regions[]': value5
},
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,24 +0,0 @@
{% 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 %}
@@ -1,93 +1,74 @@
{% 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="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>
{% 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>
</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>
<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 %}
-1
View File
@@ -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'),
)
+195 -9
View File
@@ -1,6 +1,167 @@
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 django.http import QueryDict
from django.http import HttpResponseRedirect
from .forms import DeviceFilterForm
from .filters import DeviceFilterSet
import json
from dcim.models import Device, Cable, DeviceRole, DeviceType
from extras.models import Tag
def get_topology_data(queryset, hide_unconnected):
nodes = []
nodes_ids = []
edges = []
edge_ids = 0
cable_ids = []
circuit_ids = []
if not queryset:
return None
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
device_ids = [d.id for d in queryset]
for qs_device in queryset:
device_has_connections = False
links_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_device:
if link_from.termination_a_type.name != "circuit termination" and link_from.termination_b_type.name != "circuit termination":
if link_from.termination_a_type.name not in ignore_cable_type and link_from.termination_b_type.name not in ignore_cable_type:
if link_from.id not in cable_ids:
if link_from.termination_a.device.id in device_ids and link_from.termination_b.device.id in device_ids:
device_has_connections = True
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.device.id in device_ids and link_from.termination_b.device.id in device_ids:
device_has_connections = True
else:
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
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:
if hide_unconnected == None or (hide_unconnected is True and device_has_connections is True):
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:
if qs_device.custom_field_data["coordinates"] is not None:
if ';' in qs_device.custom_field_data["coordinates"]:
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 +169,39 @@ 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
class TopologyFullView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
if request.GET:
hide_unconnected = None
if 'hide_unconnected' in request.GET:
if request.GET["hide_unconnected"] == "on" :
hide_unconnected = True
"""
Show the full view page
"""
def get(self, request):
return render(request, 'netbox_topology_views/full.html')
if 'draw_init' in request.GET:
if request.GET["draw_init"].lower() == 'true':
topo_data = get_topology_data(self.queryset, hide_unconnected)
else:
topo_data = get_topology_data(self.queryset, hide_unconnected)
else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
q_device_role_id = DeviceRole.objects.filter(name__in=preselected_device_roles).values_list('id', flat=True)
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list('name', flat=True)
q = QueryDict(mutable=True)
q.setlist('device_role_id', list(q_device_role_id))
q.setlist('tag', list(q_tags))
q['draw_init'] = settings.PLUGINS_CONFIG["netbox_topology_views"]["draw_default_layout"]
query_string = q.urlencode()
return HttpResponseRedirect(request.path + "?" + query_string)
return render(request, 'netbox_topology_views/index.html' , {
'filter_form': DeviceFilterForm(request.GET, label_suffix=''),
'topology_data': json.dumps(topo_data)
}
)
+6368 -118
View File
File diff suppressed because it is too large Load Diff
+11 -12
View File
@@ -1,28 +1,27 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "0.4.10",
"version": "1.0.0-alpha.6",
"scripts": {
"resources": "gulp build",
"resources_dev": "gulp build_dev"
},
"dependencies": {},
"dependencies": {
"vis-data": "^7.1.2",
"vis-network": "^9.1.0",
"vis-util": "^5.0.2"
},
"devDependencies": {
"gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-sass": "^4.1.0",
"gulp-uglify": "^3.0.2",
"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"
"gulp-uglify": "^3.0.2"
},
"peerDependencies": {
"@egjs/hammerjs": "^2.0.0",
"uuid": "^8.0.0",
"keycharm": "^0.4.0",
"timsort": "^0.3.0"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name='netbox-topology-views',
version='0.4.10',
version='1.0.0-alpha.6',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',