Compare commits
50
Commits
v0.4.5
...
v1.0.0-alpha.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2387e5d0cc | ||
|
|
0bcbd35082 | ||
|
|
0ce1c6df13 | ||
|
|
7dcd72a3f9 | ||
|
|
616c0ab1b5 | ||
|
|
8385dc23f8 | ||
|
|
449fdf3f80 | ||
|
|
b53ceed2ae | ||
|
|
064751f727 | ||
|
|
deedee78bd | ||
|
|
683e125cd5 | ||
|
|
3b4209223e | ||
|
|
6fc7cce79a | ||
|
|
fd4a1cd0c7 | ||
|
|
2deaed1adc | ||
|
|
55a61d4d10 | ||
|
|
3a4652543f | ||
|
|
9b4dd77943 | ||
|
|
91a690f1c8 | ||
|
|
5f498f9ce2 | ||
|
|
195ac47542 | ||
|
|
a25be917ee | ||
|
|
079361b190 | ||
|
|
a314d2b307 | ||
|
|
0340522f59 | ||
|
|
2edd597871 | ||
|
|
5f4f97c360 | ||
|
|
17ae8b4a1d | ||
|
|
bb9765e011 | ||
|
|
5bfc47775b | ||
|
|
216508170f | ||
|
|
0faa135978 | ||
|
|
9ba61cce28 | ||
|
|
9824a3f659 | ||
|
|
4115584306 | ||
|
|
cd239ac6ae | ||
|
|
030844f2fb | ||
|
|
2e6dd7de23 | ||
|
|
de5707ff06 | ||
|
|
371c5c3502 | ||
|
|
26139cbb1c | ||
|
|
4ea315d4b2 | ||
|
|
40ae715eb5 | ||
|
|
204098a143 | ||
|
|
9b606643ea | ||
|
|
f184bdf2f3 | ||
|
|
370eff6b67 | ||
|
|
6d28ce2da4 | ||
|
|
8a97923bb2 | ||
|
|
7f0dcd97f7 |
@@ -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
|
||||
@@ -1,7 +1,10 @@
|
||||
# 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,15 +12,37 @@ 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`
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -31,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
|
||||
|
||||
@@ -62,4 +88,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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -3,18 +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.5'
|
||||
description = 'An plugin to render topology maps'
|
||||
version = '1.0.0-alpha.5'
|
||||
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' : ''
|
||||
'preselected_tags' : [],
|
||||
'enable_circuit_terminations': True,
|
||||
'draw_default_layout': False
|
||||
}
|
||||
|
||||
config = TopologyViewsConfig
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""REST API"""
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -2,49 +2,29 @@ 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 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 circuits.models import Circuit
|
||||
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(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"]:
|
||||
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
|
||||
@@ -59,138 +39,22 @@ class SaveCoordsViewSet(GenericViewSet):
|
||||
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(GenericViewSet):
|
||||
#_ignore_model_permissions = True
|
||||
#permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
def _filter(self, site, role, name, tags):
|
||||
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)
|
||||
return filter_devices
|
||||
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
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"]
|
||||
|
||||
devices = self._filter(sites, devicerole, name, tags)
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
edge_ids = 0
|
||||
cable_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"] = "Connection between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||
edges.append(edge)
|
||||
else:
|
||||
pass
|
||||
#circuittermination not supported for now
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -1 +1 @@
|
||||
var graph=null,container=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"}},physics:{solver:"forceAtlas2Based"}};function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("visgraph"),csrftoken=jQuery("[name=csrfmiddlewaretoken]").val(),startLoadSearchBar(),handleButtonPress()},!1)}function startLoadSearchBar(){$("#device-roles").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"../../api/dcim/device-roles/?brief=true",dataType:"json",type:"GET",data:function(e){return{q:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}}),$("#sites").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"../../api/dcim/sites/?brief=true",dataType:"json",type:"GET",data:function(e){return{q:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}}),$("#tags").select2({allowClear:!0,placeholder:"---------",theme:"bootstrap",multiple:!0,ajax:{url:"../../api/extras/tags/?brief=true",dataType:"json",type:"GET",data:function(e){return{q:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}});var s=$("#device-roles");$.ajax({type:"GET",url:"../../api/plugins/topology-views/preselectdeviceroles/"}).then(function(e){$.each(e.results,function(e,t){var a=new Option(t.name,t.id,!0,!0);s.append(a).trigger("change"),s.trigger({type:"select2:select",params:{data:t}})})});var n=$("#tags");$.ajax({type:"GET",url:"../../api/plugins/topology-views/preselecttags/"}).then(function(e){$.each(e.results,function(e,t){var a=new Option(t.name,t.id,!0,!0);n.append(a).trigger("change"),n.trigger({type:"select2:select",params:{data:t}})})})}function handleButtonPress(){$("#search-form").submit(function(e){$("#status").html('<span class="label label-info">Loading data</span>'),e.preventDefault();var t=$("#name").val(),a=$("#device-roles").val(),s=$("#sites").val(),n=$("#tags").val();$.ajax({type:"POST",url:"../../api/plugins/topology-views/search/search/",data:JSON.stringify({name:t,devicerole:a,sites:s,tags:n}),headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){$("#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(e.nodes,function(e,t){nodes.add(t)}),$.each(e.edges,function(e,t){edges.add(t)}),graph.fit(),graph.on("afterDrawing",function(){$("#status").html('<span class="label label-success">Ready</span>')}),graph.on("dragEnd",function(e){dragged=this.getPositions(e.nodes),$.each(dragged,function(e,t){$("#coordstatus").html(""),$("#checkSaveCoordinates").is(":checked")&&$.ajax({url:"../../api/plugins/topology-views/save-coords/save_coords/",type:"POST",dataType:"json",headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",data:JSON.stringify({node_id:e,x:t.x,y:t.y}),error:function(e){$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>')},success:function(e){$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>')}})})})},error:function(e){$("#status").html('<span class="label label-warning">Something went wrong</span>')}})})}
|
||||
var graph=null,container=null;function iniPlotboxFull(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("fullvisgraph"),startRender()},!1)}function startRender(){var e=location.search;$.ajax({type:"GET",url:"../../api/plugins/topology-views/search/search/"+e,contentType:"application/json; charset=utf-8",success:function(e,n,t){graph=null,nodes=new vis.DataSet,edges=new vis.DataSet,graph=new vis.Network(container,{nodes:nodes,edges:edges},options),$.each(e.nodes,function(e,n){nodes.add(n)}),$.each(e.edges,function(e,n){edges.add(n)}),graph.fit(),canvas=document.getElementById("fullvisgraph").getElementsByTagName("canvas")[0]}})}graph=null,container=null;var downloadButton=null,MIME_TYPE="image/png",canvas=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[];function htmlTitle(e){return(container=document.createElement("div")).innerHTML=e,container}function addEdge(e){e.title=htmlTitle(e.title),edges.add(e)}function addNode(e){e.title=htmlTitle(e.title),nodes.add(e)}function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("visgraph"),handleLoadData(),downloadButton=document.getElementById("btnDownloadImage"),btnFullView=document.getElementById("btnFullView")},!1)}function handleLoadData(){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),graph.fit(),canvas=document.getElementById("visgraph").getElementsByTagName("canvas")[0],graph.on("afterDrawing",function(){var e=canvas.toDataURL(MIME_TYPE);downloadButton.href=e,downloadButton.download="topology"})}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
#visgraph {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,204 +32,51 @@ var options = {
|
||||
solver: 'forceAtlas2Based'
|
||||
}
|
||||
};
|
||||
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: "../../api/dcim/sites/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
function handleLoadData() {
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
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.fit();
|
||||
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||
|
||||
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();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "../../api/plugins/topology-views/search/search/",
|
||||
data: JSON.stringify({
|
||||
'name': value,
|
||||
'devicerole': value2,
|
||||
'sites': value3,
|
||||
'tags': value4
|
||||
}),
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
success: function (data_result) {
|
||||
$("#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();
|
||||
|
||||
graph.on('afterDrawing', function () {
|
||||
$("#status").html('<span class="label label-success">Ready</span>');
|
||||
});
|
||||
|
||||
graph.on("dragEnd", function (params) {
|
||||
dragged = this.getPositions(params.nodes);
|
||||
$.each(dragged, function (node_id, coordinates) {
|
||||
$("#coordstatus").html('');
|
||||
if ($('#checkSaveCoordinates').is(":checked")) {
|
||||
//nodes.update({ id: node_id, physics: false });
|
||||
$.ajax({
|
||||
url: "../../api/plugins/topology-views/save-coords/save_coords/",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
'node_id': node_id,
|
||||
'x': coordinates.x,
|
||||
'y': coordinates.y
|
||||
}),
|
||||
error: function (error_result) {
|
||||
$("#coordstatus").html('<span class="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,81 +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>
|
||||
{% 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>
|
||||
<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,6 +1,166 @@
|
||||
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:
|
||||
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,5 +168,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
|
||||
|
||||
if request.GET:
|
||||
hide_unconnected = None
|
||||
if 'hide_unconnected' in request.GET:
|
||||
if request.GET["hide_unconnected"] == "on" :
|
||||
hide_unconnected = True
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
Generated
+7030
-299
File diff suppressed because it is too large
Load Diff
+15
-6
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "0.4.5",
|
||||
"version": "1.0.0-alpha.45",
|
||||
"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.2.0",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"vis-network": "^7.6.4"
|
||||
"gulp-sass": "^4.1.0",
|
||||
"gulp-uglify": "^3.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"uuid": "^8.0.0",
|
||||
"keycharm": "^0.4.0",
|
||||
"timsort": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='netbox-topology-views',
|
||||
version='0.4.5',
|
||||
version='1.0.0-alpha.5',
|
||||
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