Compare commits

..
43 Commits
Author SHA1 Message Date
mattieserver 4ea2097429 bump version 2022-09-02 15:13:22 +02:00
mattieserver cfaba462d5 removed unused import (#149) 2022-09-02 15:12:36 +02:00
Mattijs Vanhaverbeke 5e2f1cca57 added info about new icons 2022-09-01 19:10:15 +02:00
Mattijs Vanhaverbeke b2e2a4c781 bump version 2022-09-01 18:48:22 +02:00
mattieserver 318b8495b7 Netbox v3.3 Support (#145)
* init

* added circuit (or at least part of)

* refactor circuits + fix js

* small changes

* small changes

* added power + rework settings

* small changes
2022-09-01 18:44:03 +02:00
mattieserver 5e1d5d2937 added always_save_coordinates (#144) 2022-08-24 14:07:26 +02:00
Andrea DaineseandMattijs Vanhaverbeke 0b6df3bd28 disable physics if not saving coords (#139)
* initial commit

* keeps x/y when not saving coords

Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2022-08-05 17:53:15 +02:00
mattieserver af1666b3ba move to esbuild & yarn (#138)
* move to esbuild & yarn

* remove gulpfile

* cleanup unused ref

* updated gitignore
2022-08-03 17:29:15 +02:00
Mattijs Vanhaverbeke 31a8c756e2 package info 2022-08-03 14:07:51 +02:00
Mattijs Vanhaverbeke ce39c98396 bump version 2022-08-03 12:07:58 +02:00
mattieserver faf156a6b2 133 enhancement bubbletext (#136)
* added extra fields to popup

* open new tab with device info
2022-08-03 11:46:37 +02:00
mattieserver 1b36b9e7df added extra check (#135) 2022-08-03 10:59:02 +02:00
mattieserver 652a44437e Create SECURITY.md 2022-07-29 09:33:15 +02:00
mattieserver 76141e70ce update gh actions 2022-06-30 22:47:55 +02:00
mattieserver 76b4f6ac9a Create docker-publish.yml 2022-06-30 22:40:18 +02:00
mattieserver 953d7480af add dockerfile (#130) 2022-06-30 22:39:18 +02:00
Mattijs Vanhaverbeke f323278f00 bump version 2022-06-30 22:22:47 +02:00
mattieserver 56a6f22bf6 added site to device tooltip (#128) 2022-06-30 22:18:35 +02:00
Mattijs Vanhaverbeke b9d3cc3c4e bump version 2022-06-30 22:06:33 +02:00
Mattijs Vanhaverbeke d4833071df added filter on device status 2022-06-30 22:03:23 +02:00
Mattijs Vanhaverbeke 877d308574 small config change 2022-06-30 19:27:11 +02:00
pycolas 83b662eac3 End to end connections (#113)
* Add hide-device-role feature

* Add hide-device-role feature

* Remove end^Cevices not in queryset

* clean up

* Fix destination.device.id test

* Query optimization

* multiple updates
2022-06-30 19:13:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 53a996edbc Bump minimist from 1.2.5 to 1.2.6 (#110)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 11:38:34 +02:00
Mattijs Vanhaverbeke 398deb5382 bump version 2022-05-10 09:28:21 +02:00
Alexander Ryazanov b5991a5f50 Improve visualization performance (substantially) (#109)
* fix: moved image generation to avoid severe performance issues

* fix: moved button finding code before data handling

* fix: fixed leftover invalid variable naming
2022-05-10 09:21:22 +02:00
mattieserver de92627176 add shadow to cables (#98)
* addes shadow to edge

* changed bg color in dark mode
2022-05-06 06:43:06 +02:00
mattieserver 6be028b9dc fix typo (#100) 2022-04-15 19:42:32 +02:00
Mattijs Vanhaverbeke 8997c38263 bump version 2022-04-11 13:12:04 +02:00
Mattijs Vanhaverbeke d927e40867 bump version 2022-03-25 13:16:00 +01:00
Jeremy Stretchandmattieserver 62c0dbddb2 Initial work on NetBox v3.2 support (#88)
Co-authored-by: mattieserver <3049868+mattieserver@users.noreply.github.com>
2022-03-25 13:13:33 +01:00
mattieserver e68a8a1af0 added tenants (#94) 2022-03-25 10:12:36 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 68e8aa0512 Bump path-parse from 1.0.6 to 1.0.7 (#74)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-18 12:15:05 +01:00
mattieserver b792128415 Fix dark mode (#90)
* fixed dark mode
2022-03-10 11:46:00 +01:00
mattieserver f5d9f589bf Fix circuit error (#87)
* fix 'Interface' object has no attribute 'circuit'
2022-03-09 21:00:55 +01:00
mattieserver 3e5b28f208 Update to support netbox v3.1 (#82)
* init for v3.1

* added coords + bump version

* fix filter + added preselected roles

* added draw_default_layout

* upgraded packages

* fixed edge/node labels

* fixed enable_circuit_terminations

* added filter for unconnected devices

* added ignore_cable_type

* fixed old settings using a string instead of array

* removed distutils

* fixed coords check

* added coor saving

* added doc img

* updated docs

* bump version

* bump version
2022-03-08 17:03:47 +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
42 changed files with 1561 additions and 5625 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
+93
View File
@@ -0,0 +1,93 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '35 10 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25
with:
cosign-release: 'v1.9.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
COSIGN_EXPERIMENTAL: "true"
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
+11 -11
View File
@@ -5,7 +5,7 @@ name: Upload Python Package
on:
release:
types: [created]
types: [published]
jobs:
deploy:
@@ -13,19 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
+2 -1
View File
@@ -128,4 +128,5 @@ dmypy.json
# Pyre type checker
.pyre/
node_modules
node_modules
app.js.map
+4
View File
@@ -0,0 +1,4 @@
FROM netboxcommunity/netbox:latest
COPY ./docker/plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
+24 -10
View File
@@ -8,7 +8,7 @@ Support to filter on name, site, tag and device role.
## Preview
![preview image](doc/img/preview.png?raw=true "preview")
![preview image](doc/img/preview_3.1.jpeg?raw=true "preview")
## Install
@@ -35,6 +35,10 @@ Then run `python3 manage.py collectstatic --no-input`
| netbox version | netbox-topology-views version |
| ------------- |-------------|
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
| >= 2.11.1 | >= v0.5.3 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
@@ -42,7 +46,7 @@ Then run `python3 manage.py collectstatic --no-input`
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.
If you create a custom field "coordinates" for "dcim > device" and "Circuits > circuit" with type "text" and name "coordinates" you will see the same layout every time.
The coordinates can then be provided as: "X;Y"
@@ -54,20 +58,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 |
| enable_circuit_terminations | False | (bool) Set to true if you want to see circuit terminations in the topology |
| always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default |
| ignore_cable_type | [] | The cable types that you want to ignore in the views |
| preselected_tags | '[]' | The name of tags you want to preload |
| 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
@@ -77,7 +82,7 @@ If you add your own image you also need to add the slug to the `device_img` sett
## Use
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/topology-views/` to view your topologies
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies
### Update
@@ -96,3 +101,12 @@ To view `/plugins/topology-views/` you need the following permissions:
+ dcim | site | can view site
+ extras | tag | can view tag
+ dcim | device role | can view device role
## Icons info
Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power).
Power icons created by [Flat Icons - Flaticon](https://www.flaticon.com/free-icons/power)
Provider icons created by [Good Ware - Flaticon](https://www.flaticon.com/free-icons/provider)
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 2.0 | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Please create an issue when you encounter a Security issue.
+2 -1
View File
@@ -2,6 +2,7 @@
+ install docker
+ install docker compose
+ install npm
+ enable yarn
+ clone netbox repo
+ clone netbox_topology_views repo
+ create venv: `/usr/bin/python3 -m venv venv`
@@ -12,7 +13,7 @@
# Build
+ build the package: `python3 setup.py develop`
+ build the resources `npm run resources`
+ build the resources `yarn run bundle`
# Test
+ copy configuration.py from dev_setup to the netbox/netbox/ folder
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+1
View File
@@ -0,0 +1 @@
netbox-topology-views
-78
View File
@@ -1,78 +0,0 @@
var gulp = require('gulp');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var cleanCss = require('gulp-clean-css');
var jsVendorFiles = [
'node_modules/vis-network/standalone/umd/vis-network.min.js',
];
var cssVendorFiles = [
'node_modules/vis-network/dist/dist/vis-network.min.css',
];
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
var jsLocalResourceFiles = './netbox_topology_views/static_dev/js/*.js',
jsDest = './netbox_topology_views/static/netbox_topology_views/js/';
function styles_local() {
return gulp.src(sassFiles)
.pipe(sass().on('error', sass.logError))
.pipe(concat('app.css'))
.pipe(cleanCss())
.pipe(gulp.dest(cssDest));
}
exports.styles_local = styles_local
function styles_vendor() {
return gulp.src(cssVendorFiles)
.pipe(concat('vendor.css'))
.pipe(cleanCss())
.pipe(gulp.dest(cssDest))
}
exports.styles_vendor = styles_vendor
function js_local() {
return gulp.src(jsLocalResourceFiles)
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest(jsDest))
}
exports.js_local = js_local
function js_vendor() {
return gulp.src(jsVendorFiles)
.pipe(concat('vendor.js'))
.pipe(uglify())
.pipe(gulp.dest(jsDest))
}
exports.js_vendor = js_vendor
function js_local_dev() {
return gulp.src(jsLocalResourceFiles)
.pipe(concat('app.js'))
.pipe(gulp.dest(jsDest))
}
exports.js_local_dev = js_local_dev
function js_vendor_dev() {
return gulp.src(jsVendorFiles)
.pipe(concat('vendor.js'))
.pipe(gulp.dest(jsDest))
}
exports.js_vendor_dev = js_vendor_dev
exports.css = gulp.series(styles_local, styles_vendor);
exports.js = gulp.series(js_local, js_vendor);
exports.js_dev = gulp.series(js_local_dev, js_vendor_dev);
exports.build = gulp.series(exports.css, exports.js);
exports.build_dev = gulp.series(exports.css, exports.js_dev);
+8 -7
View File
@@ -4,18 +4,19 @@ class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render topology maps'
version = '0.5.1'
version = '3.0.0-beta.2'
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': [],
'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
'always_save_coordinates': False,
'preselected_tags' : [],
'draw_default_layout': False
}
config = TopologyViewsConfig
-3
View File
@@ -1,3 +0,0 @@
from django.contrib import admin
#no models yet
-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):
+2 -7
View File
@@ -1,13 +1,8 @@
from netbox.api import OrderedDefaultRouter
from rest_framework import routers
from . import views
router = OrderedDefaultRouter()
router.APIRootView = views.TopologyViewsRootView
router = routers.DefaultRouter()
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
router.register('preselecttags', views.PreSelectTagsViewSet)
router.register('search', views.SearchViewSet, basename='search')
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
urlpatterns = router.urls
+14 -214
View File
@@ -4,34 +4,18 @@ 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 dcim.models import DeviceRole, Device, Cable
from dcim.models import DeviceRole, Device, Cable , PowerPanel, PowerFeed
from circuits.models import Circuit
from extras.models import Tag
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(",")
class TopologyViewsRootView(APIRootView):
def get_view_name(self):
return 'TopologyViews'
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
serializer_class = PreDeviceRoleSerializer
class PreSelectTagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.filter(name__in=preselected_tags)
serializer_class = PreTagSerializer
class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all()
@@ -53,8 +37,18 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
if "y" in request.data:
if request.data["y"]:
y_coord = request.data["y"]
actual_device= Device.objects.get(id=device_id)
if device_id.startswith("c"):
device_id = device_id.lstrip('c')
actual_device= Circuit.objects.get(id=device_id)
elif device_id.startswith("p"):
device_id = device_id.lstrip('p')
actual_device= PowerPanel.objects.get(id=device_id)
elif device_id.startswith("f"):
device_id = device_id.lstrip('f')
actual_device= PowerFeed.objects.get(id=device_id)
else:
actual_device= Device.objects.get(id=device_id)
if "coordinates" in actual_device.custom_field_data:
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
@@ -74,197 +68,3 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
results["status"] = "not allowed to save coords"
return Response(results, status=500)
class SearchViewSet(ReadOnlyModelViewSet):
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
hide_unconnected = request.query_params.get('hide_unconnected', None)
if hide_unconnected == "":
hide_unconnected = None
print(hide_unconnected)
if hide_unconnected == 'false':
print("ok")
devices = self._filter(sites, devicerole, name, tags, regions)
nodes = []
edges = []
edge_ids = 0
cable_ids = []
circuit_ids = []
for device in devices:
cables = device.get_cables()
if cables.exists():
device_has_connections = True
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)
else:
device_has_connections = False
if hide_unconnected == 'false' or (hide_unconnected == 'true' and device_has_connections is True):
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
if "coordinates" in device.custom_field_data:
if device.custom_field_data["coordinates"] != "":
cords = 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 Response(results)
+59
View File
@@ -0,0 +1,59 @@
from cProfile import label
import django_filters
from django.db.models import Q
from dcim.models import Device, DeviceRole, Region, Site, Location
from dcim.choices import DeviceStatusChoices
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.models import TenantGroup, Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
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)',
)
status = django_filters.MultipleChoiceFilter(
choices=DeviceStatusChoices,
null_value=None
)
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)
+83
View File
@@ -0,0 +1,83 @@
from cProfile import label
import imp
from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from dcim.models import Device, Site, Region, DeviceRole, Location
from django import forms
from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, MultipleChoiceField)
allow_coordinates_saving = bool(settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"])
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
fieldsets = (
(None, ('q', 'hide_unconnected', 'save_coords', 'show_circuit', 'show_power' ,)),
(None, ('tenant_group_id', 'tenant_id')),
(None, ('region_id', 'site_id', 'location_id')),
(None, ('device_role_id', )),
(None, ('tag', 'status')),
)
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
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"),
required=False,
initial=False
)
show_power = forms.BooleanField(
label=_("Show Power panel/feed"),
required=False,
initial=False
)
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not allow_coordinates_saving)
)
status = MultipleChoiceField(
choices=DeviceStatusChoices,
required=False,
label=_("Device Status")
)
tag = TagFilterField(model)
-3
View File
@@ -1,3 +0,0 @@
from django.db import models
# no modeles yet
+2 -3
View File
@@ -4,7 +4,6 @@ from utilities.choices import ButtonColorChoices
menu_items = (
PluginMenuItem(
link='plugins:netbox_topology_views:home',
link_text='Topology',
buttons=()
link_text='Topology'
),
)
)
@@ -1 +1 @@
#visgraph{height:70vh}
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,87 @@
const esbuild = require('esbuild');
const { sassPlugin } = require('esbuild-sass-plugin');
const options = {
bundle: true,
minify: true,
sourcemap: 'external',
sourcesContent: false,
logLevel: 'error',
};
const ARGS = process.argv.slice(2);
async function bundleScripts() {
const entryPoints = {
'app': 'js/home.js'
};
try {
const result = await esbuild.build({
...options,
outdir: '../static/netbox_topology_views/js/',
entryPoints,
target: 'es2016',
});
if (result.errors.length === 0) {
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
const source = sourceName.split('/')[1];
console.log(`✅ Bundled source file '${source}' to '${targetName}.js'`);
}
}
} catch (err) {
console.error(err);
}
}
async function bundleStyles() {
try {
const entryPoints = {
'vendor': 'css/_external.scss',
'app': 'css/app.scss',
};
const pluginOptions = { outputStyle: 'compressed' };
// Allow cache disabling.
if (ARGS.includes('--no-cache')) {
pluginOptions.cache = false;
}
let result = await esbuild.build({
...options,
outdir: '../static/netbox_topology_views/css/',
// Disable sourcemaps for CSS/SCSS files, see #7068
sourcemap: false,
entryPoints,
plugins: [sassPlugin(pluginOptions)],
loader: {
'.eot': 'file',
'.woff': 'file',
'.woff2': 'file',
'.svg': 'file',
'.ttf': 'file',
},
});
if (result.errors.length === 0) {
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
const source = sourceName.split('/')[1];
console.log(`✅ Bundled source file '${source}' to '${targetName}.css'`);
}
}
} catch (err) {
console.error(err);
}
}
async function bundleAll() {
if (ARGS.includes('--styles')) {
// Only run style jobs.
return await bundleStyles();
} else if (ARGS.includes('--scripts')) {
// Only run script jobs.
return await bundleScripts();
}
await bundleStyles();
await bundleScripts();
}
bundleAll();
@@ -0,0 +1 @@
@import '../node_modules/vis-network/dist/dist/vis-network.min.css';
@@ -1,3 +1,7 @@
#visgraph {
height: 70vh;
}
html[data-netbox-color-mode=dark] #visgraph {
background-color: #212529;
}
@@ -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];
}
});
}
+144 -272
View File
@@ -1,11 +1,16 @@
import { DataSet } from "vis-data/esnext";
import { Network } from "vis-network/esnext";
//import 'vis-util';
var graph = null;
var container = null;
var downloadButton = null;
var MIME_TYPE = "image/png";
const MIME_TYPE = "image/png";
var canvas = null;
var csrftoken = null;
var nodes = new vis.DataSet();
var edges = new vis.DataSet();
var nodes = new DataSet();
var edges = new DataSet();
var options = {
interaction: {
hover: true,
@@ -27,289 +32,156 @@ var options = {
font: {
face: 'helvetica',
},
shadow: {
enabled: true
}
},
physics: {
solver: 'forceAtlas2Based'
}
};
var selected_regions = [];
var coord_save_checkbox = null;
var htmlElement = null;
function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () {
container = document.getElementById('visgraph');
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
startLoadSearchBar();
handleButtonPress();
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
}
})
};
export function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
});
$('#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;
}
return cookieValue;
};
export function htmlTitle(html) {
container = document.createElement("div");
container.innerHTML = html;
return container;
};
export function addEdge(item) {
item.title = htmlTitle(item.title);
edges.add(item);
};
export function addNode(item) {
item.title = htmlTitle(item.title);
nodes.add(item);
}
export function iniPlotboxIndex() {
csrftoken = getCookie('csrftoken');
container = document.getElementById('visgraph');
htmlElement = document.getElementsByTagName("html")[0];
downloadButton = document.getElementById('btnDownloadImage');
handleLoadData();
btnFullView = document.getElementById('btnFullView');
coord_save_checkbox = document.getElementById('id_save_coords');
};
export function performGraphDownload() {
var tempDownloadLink = document.createElement('a');
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
tempDownloadLink.href = generatedImageUrl;
tempDownloadLink.download = "topology";
document.body.appendChild(tempDownloadLink);
tempDownloadLink.click();
document.body.removeChild(tempDownloadLink);
};
export function handleLoadData() {
if (topology_data !== null) {
if (htmlElement.dataset.netboxColorMode == "dark") {
options.nodes.font.color = "#fff";
}
graph = null;
nodes = new DataSet();
edges = new DataSet();
graph = new 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];
downloadButton.onclick = function(e) { performGraphDownload(); return false; };
graph.on("dragEnd", function (params) {
dragged = this.getPositions(params.nodes);
if (coord_save_checkbox.checked) {
if (Object.keys(dragged).length !== 0) {
for (dragged_device in dragged) {
var node_id = dragged_device;
var url = "/api/plugins/netbox_topology_views/save-coords/save_coords/";
var xhr = new XMLHttpRequest();
xhr.open("PATCH", url);
xhr.setRequestHeader('X-CSRFToken', csrftoken );
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log(xhr.status);
}};
var data = JSON.stringify({
'node_id': node_id,
'x': dragged[node_id].x,
'y': dragged[node_id].y});
xhr.send(data);
}
}
}
});
graph.on("doubleClick", function (params) {
let selected_devices = params.nodes;
for (let selected_device in selected_devices) {
let url = ""
if(String(selected_devices[selected_device]).startsWith("c")) {
cid = selected_devices[selected_device].substring(1);
url = "/circuits/circuits/" + cid + "/";
}
else if (String(selected_devices[selected_device]).startsWith("p")) {
cid = selected_devices[selected_device].substring(1);
url = "/dcim/power-panels/" + cid + "/";
}
else if (String(selected_devices[selected_device]).startsWith("f")) {
cid = selected_devices[selected_device].substring(1);
url = "/dcim/power-feeds/" + cid + "/";
}
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;
url = "/dcim/devices/" + selected_devices[selected_device] + "/";
}
},
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
}
})
};
window.open(url, "_blank");
}
}
});
$('#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
}
})
};
}
}
});
$('#regions').on('select2:select', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
if (index == -1) {
selected_regions.push(data.id.toString());
}
});
$('#regions').on('select2:unselect', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
console.log(index);
if (index > -1) {
selected_regions.splice(index, 1);
}
});
$('#regions').on('select2:clear', function (e) {
selected_regions = [];
});
var deviceRolesSelect = $('#device-roles');
$.ajax({
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
}
});
});
});
}
};
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
}
});
});
});
}
export function load_doc() {
if (document.readyState !== 'loading') {
iniPlotboxIndex();
} else {
document.addEventListener('DOMContentLoaded', iniPlotboxIndex);
}
};
function handleButtonPress() {
$("#search-form").submit(function (event) {
$("#status").html('<span class="label label-info">Loading data</span>');
event.preventDefault();
var value = $("#name").val();
var value2 = $("#device-roles").val();
var value3 = $("#sites").val();
var value4 = $("#tags").val();
var value5 = $("#regions").val();
var value6 = $('#checkHideUnconnected').is(":checked");
$.ajax({
type: "GET",
url: "../../api/plugins/topology-views/search/search/",
data: {
'name': value,
'devicerole[]': value2,
'sites[]': value3,
'tags[]': value4,
'regions[]': value5,
'hide_unconnected': value6
},
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
dataType: "json",
beforeSend: function(){
new_url = this.url.split("../../api/plugins/topology-views/search/search/?");
new_url = new_url[1];
new_url = "../../plugins/topology-views/full?" + new_url
btnFullView.classList.remove("disabled");
btnFullView.href = new_url;
},
success: function (data_result, status, xhr) {
$("#status").html('<span class="label label-info">Drawing network</span>');
graph = null;
nodes = new vis.DataSet();
edges = new vis.DataSet();
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
$.each(data_result["nodes"], function (index, device) {
nodes.add(device);
});
$.each(data_result["edges"], function (index, edge) {
edges.add(edge);
});
graph.fit();
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
graph.on('afterDrawing', function () {
$("#status").html('<span class="label label-success">Ready</span>');
var image = canvas.toDataURL(MIME_TYPE);
downloadButton.href = image;
downloadButton.download = "topology";
});
graph.on("dragEnd", function (params) {
dragged = this.getPositions(params.nodes);
$.each(dragged, function (node_id, coordinates) {
$("#coordstatus").html('');
if ($('#checkSaveCoordinates').is(":checked")) {
//nodes.update({ id: node_id, physics: false });
$.ajax({
url: "../../api/plugins/topology-views/save-coords/save_coords/",
type: 'PATCH',
dataType: 'json',
headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8",
processData: false,
data: JSON.stringify({
'node_id': node_id,
'x': coordinates.x,
'y': coordinates.y
}),
error: function (error_result) {
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
},
success: function (data_result) {
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
},
});
}
});
});
},
error: function (error_result) {
$("#status").html('<span class="label label-warning">Something went wrong</span>');
},
});
});
}
load_doc();
@@ -0,0 +1,25 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "3.0.0-beta.1",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
"bundle:scripts": "node bundle.js --scripts"
},
"dependencies": {
"vis-data": "^7.1.4",
"vis-network": "^9.1.2",
"vis-util": "^5.0.3"
},
"devDependencies": {
"esbuild": "^0.12.24",
"esbuild-sass-plugin": "^1.5.2",
"@egjs/hammerjs": "^2.0.0",
"component-emitter": "^1.3.0",
"keycharm": "^0.4.0",
"timsort": "^0.3.0",
"uuid": "^8.0.0"
}
}
+356
View File
@@ -0,0 +1,356 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@egjs/hammerjs@^2.0.0":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
dependencies:
"@types/hammerjs" "^2.0.36"
"@esbuild/linux-loong64@0.14.53":
version "0.14.53"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==
"@types/hammerjs@^2.0.36":
version "2.0.41"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
esbuild-android-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675"
integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==
esbuild-android-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==
esbuild-darwin-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==
esbuild-darwin-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==
esbuild-freebsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==
esbuild-freebsd-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==
esbuild-linux-32@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==
esbuild-linux-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==
esbuild-linux-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==
esbuild-linux-arm@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==
esbuild-linux-mips64le@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==
esbuild-linux-ppc64le@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==
esbuild-linux-riscv64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==
esbuild-linux-s390x@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==
esbuild-netbsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52"
integrity sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==
esbuild-openbsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==
esbuild-sass-plugin@^1.5.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz#76babbd6d4c87ffd14259fd364d446f857b3f513"
integrity sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==
dependencies:
esbuild "^0.14.5"
picomatch "^2.3.0"
resolve "^1.20.0"
sass "^1.45.0"
esbuild-sunos-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==
esbuild-windows-32@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==
esbuild-windows-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==
esbuild-windows-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==
esbuild@^0.12.24:
version "0.12.29"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.29.tgz#be602db7c4dc78944a9dbde0d1ea19d36c1f882d"
integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==
esbuild@^0.14.5:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4"
integrity sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==
optionalDependencies:
"@esbuild/linux-loong64" "0.14.53"
esbuild-android-64 "0.14.53"
esbuild-android-arm64 "0.14.53"
esbuild-darwin-64 "0.14.53"
esbuild-darwin-arm64 "0.14.53"
esbuild-freebsd-64 "0.14.53"
esbuild-freebsd-arm64 "0.14.53"
esbuild-linux-32 "0.14.53"
esbuild-linux-64 "0.14.53"
esbuild-linux-arm "0.14.53"
esbuild-linux-arm64 "0.14.53"
esbuild-linux-mips64le "0.14.53"
esbuild-linux-ppc64le "0.14.53"
esbuild-linux-riscv64 "0.14.53"
esbuild-linux-s390x "0.14.53"
esbuild-netbsd-64 "0.14.53"
esbuild-openbsd-64 "0.14.53"
esbuild-sunos-64 "0.14.53"
esbuild-windows-32 "0.14.53"
esbuild-windows-64 "0.14.53"
esbuild-windows-arm64 "0.14.53"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
immutable@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
keycharm@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/keycharm/-/keycharm-0.4.0.tgz#8d684ea9cc01379a07fbddee33ff32d97f5ae2a7"
integrity sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve@^1.20.0:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
sass@^1.45.0:
version "1.54.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.1.tgz#4f72ef57ce2a6c3251f4e2c75eee9a0c19e09eb5"
integrity sha512-GHJJr31Me32RjjUBagyzx8tzjKBUcDwo5239XANIRBq0adDu5iIG0aFO0i/TBb/4I9oyxkEv44nq/kL1DxdDhA==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
vis-data@^7.1.4:
version "7.1.4"
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.4.tgz#90e5e796a79e1901de14c0808fb32a1a0735c1dc"
integrity sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==
vis-network@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-9.1.2.tgz#ddaca300a9764bbf1646a0aa03eb28fe55823ca8"
integrity sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==
vis-util@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-5.0.3.tgz#6a9d67c6ed92f2bfa67e50c20166bb5f59ac07bf"
integrity sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==
@@ -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,99 +1,73 @@
{% extends 'base.html' %}
{% extends 'base/layout.html' %}
{% load buttons %}
{% load render_table from django_tables2 %}
{% load static %}
{% load perms %}
{% load helpers %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
{% block title %}Topology Views{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
{% endblock %}
<div class="row ">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Network</strong> <span id="status"></span>
<div class="buttons pull-right">
<a id="btnDownloadImage" class="btn btn-xs btn-info">
<i class="mdi mdi-download"></i>
</a>
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
<i class="mdi mdi-share"></i>
</a>
</div>
</div>
<div class="panel-body">
<div id="visgraph" class=""></div>
</div>
</div>
{% block controls %}
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<i class="mdi mdi-download"></i>
Download
</a>
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form id="search-form" class="form" autocomplete="off">
{% csrf_token %}
<div class="form-group">
<label for="name">Name</label>
<input id="name" class="form-control" type="text">
</div>
<div class="form-group">
<label for="device-roles">Device roles</label>
<select class="form-control" multiple="multiple" id="device-roles"></select>
</div>
<div class="form-group">
<label for="sites">Sites</label>
<select class="form-control" multiple="multiple" id="sites"></select>
</div>
<div class="form-group">
<label for="tags">Tags</label>
<select class="form-control" multiple="multiple" id="tags"></select>
</div>
<div class="form-group">
<label for="regions">Regions</label>
<select class="form-control" multiple="multiple" id="regions"></select>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="checkHideUnconnected">
<label class="custom-control-label" for="checkHideUnconnected">Hide Unconnected</label>
</div>
</div>
<hr class="mb-4">
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
</form>
</div>
{% endblock controls %}
</div>
{% 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_color="blue" %}{% endif %}
</button>
</li>
{% endif %}
{% endblock tab_items %}
</ul>
{% endblock tabs %}
{% 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>
{% block content-wrapper %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
{% endwith %}
{% endblock %}
<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>
{% endwith %}
{% endblock content-wrapper %}
{% 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 %}
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
{% endblock javascript %}
-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'),
)
+366 -10
View File
@@ -1,21 +1,377 @@
from platform import node
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, CableTermination, DeviceRole, PathEndpoint, Interface, FrontPort, RearPort, PowerPanel, PowerFeed
from circuits.models import CircuitTermination, ProviderNetwork
from wireless.models import WirelessLink
from extras.models import Tag
supported_termination_types = ["interface", "front port", "rear port", "power outlet", "power port"]
def create_node(device, save_coords, circuit = None, powerpanel = None, powerfeed= None):
node = {}
node_content = ""
if circuit:
dev_name = "Circuit " + str(device.cid)
node["image"] = "../../static/netbox_topology_views/img/circuit.png"
node["id"] = "c{}".format(device.id)
if device.provider is not None:
node_content += "<tr><th>Provider: </th><td>" + device.provider.name + "</td></tr>"
if device.type is not None:
node_content += "<tr><th>Type: </th><td>" + device.type.name + "</td></tr>"
elif powerpanel:
dev_name = "Power Panel " + str(device.id)
node["image"] = "../../static/netbox_topology_views/img/power-panel.png"
node["id"] = "p{}".format(device.id)
if device.site is not None:
node_content += "<tr><th>Site: </th><td>" + device.site.name + "</td></tr>"
if device.location is not None:
node_content += "<tr><th>Location: </th><td>" + device.location.name + "</td></tr>"
elif powerfeed:
dev_name = "Power Feed " + str(device.id)
node["image"] = "../../static/netbox_topology_views/img/power-feed.png"
node["id"] = "f{}".format(device.id)
if device.power_panel is not None:
node_content += "<tr><th>Power Panel: </th><td>" + device.power_panel.name + "</td></tr>"
if device.type is not None:
node_content += "<tr><th>Type: </th><td>" + device.type + "</td></tr>"
if device.supply is not None:
node_content += "<tr><th>Supply: </th><td>" + device.supply + "</td></tr>"
if device.phase is not None:
node_content += "<tr><th>Phase: </th><td>" + device.phase + "</td></tr>"
if device.amperage is not None:
node_content += "<tr><th>Amperage: </th><td>" + str(device.amperage )+ "</td></tr>"
if device.voltage is not None:
node_content += "<tr><th>Voltage: </th><td>" + str(device.voltage) + "</td></tr>"
else:
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
if device.device_type is not None:
node_content += "<tr><th>Type: </th><td>" + device.device_type.model + "</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>"
if device.site is not None:
node_content += "<tr><th>Site: </th><td>" + device.site.name + "</td></tr>"
if device.location is not None:
node_content += "<tr><th>Location: </th><td>" + device.location.name + "</td></tr>"
if device.rack is not None:
node_content += "<tr><th>Rack: </th><td>" + device.rack.name + "</td></tr>"
if device.position is not None:
if device.face is not None:
node_content += "<tr><th>Position: </th><td> {} ({}) </td></tr>".format(device.position, device.face)
else:
node_content += "<tr><th>Position: </th><td>" + device.position + "</td></tr>"
node["id"] = device.id
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
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
node["title"] = dev_title
node["name"] = dev_name
node["label"] = dev_name
node["shape"] = "image"
node["physics"] = True
if "coordinates" in device.custom_field_data:
if device.custom_field_data["coordinates"] is not None:
if ";" in device.custom_field_data["coordinates"]:
cords = device.custom_field_data["coordinates"].split(";")
node["x"] = int(cords[0])
node["y"] = int(cords[1])
node["physics"] = False
else:
if save_coords:
node["physics"] = False
else:
node["physics"] = True
return node
def create_edge(edge_id, termination_a, termination_b, circuit = None, cable = None, wireless = None, power=None):
cable_a_name = "device A name unknown" if termination_a["termination_name"] is None else termination_a["termination_name"]
cable_a_dev_name = "device A name unknown" if termination_a["termination_device_name"] is None else termination_a["termination_device_name"]
cable_b_name= "device A name unknown" if termination_b["termination_name"] is None else termination_b["termination_name"]
cable_b_dev_name = "cable B name unknown" if termination_b["termination_device_name"] is None else termination_b["termination_device_name"]
edge = {}
edge["id"] = edge_id
edge["from"] = termination_a["device_id"]
edge["to"] = termination_b["device_id"]
if circuit is not None:
edge["dashes"] = True
edge["title"] = "Circuit provider: " + circuit["provider_name"] + "<br>"
edge["title"] += "Termination between <br>"
edge["title"] += cable_b_dev_name + " [" + cable_b_name + "]<br>"
edge["title"] += cable_a_dev_name + " [" + cable_a_name + "]"
elif wireless is not None:
edge["dashes"] = [2, 10, 2, 10]
edge["title"] = "Wireless Connection between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
elif power is not None:
edge["dashes"] = [5, 5, 3, 3]
edge["title"] = "Power Connection between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
else:
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
if cable is not None and cable.color != "":
edge["color"] = "#" + cable.color
return edge
def create_circuit_termination(termination):
if isinstance(termination, CircuitTermination):
return { "termination_name": termination.circuit.provider.name, "termination_device_name": termination.circuit.cid, "device_id": "c{}".format(termination.circuit.id) }
if isinstance(termination, Interface) or isinstance(termination, FrontPort) or isinstance(termination, RearPort):
return { "termination_name": termination.name, "termination_device_name": termination.device.name, "device_id": termination.device.id }
return None
def get_topology_data(queryset, hide_unconnected, save_coords, show_circuit, show_power):
nodes_devices = {}
edges = []
nodes = []
edge_ids = 0
nodes_circuits = {}
nodes_powerpanel = {}
nodes_powerfeed = {}
nodes_provider_networks = {}
cable_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]
if show_circuit:
site_ids = [d.site.id for d in queryset]
circuits = CircuitTermination.objects.filter( Q(site_id__in=site_ids) | Q( provider_network__isnull=False) ).prefetch_related("provider_network", "circuit")
for circuit in circuits:
if not hide_unconnected and circuit.circuit.id not in nodes_circuits:
nodes_circuits[circuit.circuit.id] = circuit.circuit
termination_a = {}
termination_b = {}
circuit_model = {}
if circuit.cable is not None:
termination_a = create_circuit_termination(circuit.cable.a_terminations[0])
termination_b = create_circuit_termination(circuit.cable.b_terminations[0])
elif circuit.provider_network is not None:
if circuit.provider_network.id not in nodes_provider_networks:
nodes_provider_networks[circuit.provider_network.id] = circuit.provider_network
if bool(termination_a) and bool(termination_b):
circuit_model = {"provider_name": circuit.circuit.provider.name}
edge_ids += 1
edges.append(create_edge(edge_id=edge_ids,circuit=circuit_model, termination_a=termination_a, termination_b=termination_b))
circuit_has_connections = False
for termination in [circuit.cable.a_terminations[0], circuit.cable.b_terminations[0]]:
if not isinstance(termination, CircuitTermination):
if termination.device.id not in nodes_devices and termination.device.id in device_ids:
nodes_devices[termination.device.id] = termination.device
circuit_has_connections = True
else:
if termination.device.id in device_ids:
circuit_has_connections = True
if circuit_has_connections and hide_unconnected:
if circuit.circuit.id not in nodes_circuits:
nodes_circuits[circuit.circuit.id] = circuit.circuit
for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords, circuit=True))
links = CableTermination.objects.filter( Q(_device_id__in=device_ids) ).select_related("termination_type")
wlan_links = WirelessLink.objects.filter( Q(_interface_a_device_id__in=device_ids) & Q(_interface_b_device_id__in=device_ids))
if show_power:
power_panels = PowerPanel.objects.filter( Q (site_id__in=site_ids))
power_panels_ids = [d.id for d in power_panels]
power_feeds = PowerFeed.objects.filter( Q (power_panel_id__in=power_panels_ids))
for power_feed in power_feeds:
if not hide_unconnected or (hide_unconnected and power_feed.cable_id is not None):
if power_feed.power_panel.id not in nodes_powerpanel:
nodes_powerpanel[power_feed.power_panel.id] = power_feed.power_panel
if power_feed.id not in nodes_powerfeed:
if hide_unconnected:
if power_feed.link_peers[0].device.id in device_ids:
nodes_powerfeed[power_feed.id] = power_feed
else:
nodes_powerfeed[power_feed.id] = power_feed
edge_ids += 1
termination_a = { "termination_name": power_feed.power_panel.name, "termination_device_name": str(power_feed.power_panel.id), "device_id": "p{}".format(power_feed.power_panel.id) }
termination_b = { "termination_name": power_feed.name, "termination_device_name": str(termination.id), "device_id": "f{}".format(power_feed.id) }
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, power=True))
if power_feed.cable_id is not None:
if power_feed.cable.id not in cable_ids:
cable_ids[power_feed.cable.id] = {}
cable_ids[power_feed.cable.id][power_feed.cable_end] = termination_b
for d in nodes_powerfeed.values():
nodes.append(create_node(d, save_coords, powerfeed = True))
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords, powerpanel = True))
for link in links:
if link.termination_type.name in ignore_cable_type :
continue
#Normal device cables
if link.termination_type.name in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable.id not in cable_ids:
cable_ids[link.cable.id] = {}
else:
if cable_ids[link.cable.id]['B'] is not None:
complete_link = True
elif link.cable_end == "B":
if link.cable.id not in cable_ids:
cable_ids[link.cable.id] = {}
else:
if cable_ids[link.cable.id]['A'] is not None:
complete_link = True
else:
print("Unkown cable end")
cable_ids[link.cable.id][link.cable_end] = link
if complete_link:
edge_ids += 1
if isinstance(cable_ids[link.cable.id]["B"], CableTermination):
if cable_ids[link.cable.id]["B"]._device_id not in nodes_devices:
nodes_devices[cable_ids[link.cable.id]["B"]._device_id] = cable_ids[link.cable.id]["B"].termination.device
termination_b = { "termination_name": cable_ids[link.cable.id]["B"].termination.name, "termination_device_name": cable_ids[link.cable.id]["B"].termination.device.name, "device_id": cable_ids[link.cable.id]["B"].termination.device.id }
else:
termination_b = cable_ids[link.cable.id]["B"]
if isinstance(cable_ids[link.cable.id]["A"], CableTermination):
if cable_ids[link.cable.id]["A"]._device_id not in nodes_devices:
nodes_devices[cable_ids[link.cable.id]["A"]._device_id] = cable_ids[link.cable.id]["A"].termination.device
termination_a = { "termination_name": cable_ids[link.cable.id]["A"].termination.name, "termination_device_name": cable_ids[link.cable.id]["A"].termination.device.name, "device_id": cable_ids[link.cable.id]["A"].termination.device.id }
else:
termination_a = cable_ids[link.cable.id]["A"]
edges.append(create_edge(edge_id=edge_ids, cable=link.cable, termination_a=termination_a, termination_b=termination_b))
for wlan_link in wlan_links:
if wlan_link.interface_a.device.id not in nodes_devices:
nodes_devices[wlan_link.interface_a.device.id] = wlan_link.interface_a.device
if wlan_link.interface_b.device.id not in nodes_devices:
nodes_devices[wlan_link.interface_b.device.id] = wlan_link.interface_b.device
termination_a = {"termination_name": wlan_link.interface_a.name, "termination_device_name": wlan_link.interface_a.device.name, "device_id": wlan_link.interface_a.device.id}
termination_b = {"termination_name": wlan_link.interface_b.name, "termination_device_name": wlan_link.interface_b.device.name, "device_id": wlan_link.interface_b.device.id}
wireless = {"ssid": wlan_link.ssid }
edge_ids += 1
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b,wireless=wireless))
for qs_device in queryset:
if qs_device.id not in nodes_devices and not hide_unconnected:
nodes_devices[qs_device.id] = qs_device
results = {}
for d in nodes_devices.values():
nodes.append(create_node(d, save_coords))
results["nodes"] = nodes
results["edges"] = edges
return results
class TopologyHomeView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
permission_required = ("dcim.view_site", "dcim.view_device")
"""
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().select_related("device_type", "device_role")
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:
save_coords = False
if 'save_coords' in request.GET:
if request.GET["save_coords"] == "on":
save_coords = True
hide_unconnected = False
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')
show_power = False
if "show_power" in request.GET:
if request.GET["show_power"] == "on" :
show_power = True
show_circuit = False
if "show_circuit" in request.GET:
if request.GET["show_circuit"] == "on" :
show_circuit = True
if "draw_init" in request.GET:
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(self.queryset, hide_unconnected, save_coords, show_circuit, show_power)
else:
topo_data = get_topology_data(self.queryset, hide_unconnected, save_coords, show_circuit, show_power)
else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
always_save_coordinates = bool(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"])
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"]
if always_save_coordinates:
q["save_coords"] = "on"
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)
}
)
-4817
View File
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "0.5.1",
"scripts": {
"resources": "gulp build",
"resources_dev": "gulp build_dev"
},
"dependencies": {},
"devDependencies": {
"@egjs/hammerjs": "^2.0.0",
"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",
"keycharm": "^0.3.0",
"moment": "^2.24.0",
"timsort": "^0.3.0",
"uuid": "7.0.0",
"vis-data": "^6.2.1",
"vis-network": "^7.10.2",
"vis-util": "^4.0.0"
},
"peerDependencies": {}
}
+8 -1
View File
@@ -1,16 +1,23 @@
from setuptools import setup, find_packages
from os import path
top_level_directory = path.abspath(path.dirname(__file__))
with open(path.join(top_level_directory, 'README.md'), encoding='utf-8') as file:
long_description = file.read()
setup(
name='netbox-topology-views',
version='0.5.1',
version='3.0.0-beta.2',
description='An NetBox plugin to create Topology maps',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',
license='Apache 2.0',
install_requires=[],
packages=find_packages(),
include_package_data=True,
keywords=['netbox-plugin'],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',