Compare commits
73
Commits
v0.3.1
...
1.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 | ||
|
|
0d7052d83d | ||
|
|
e50945f8b5 | ||
|
|
987aab772f | ||
|
|
2ef6e7cb96 | ||
|
|
570d241b7f | ||
|
|
2025bba6a1 | ||
|
|
22ec4fb310 | ||
|
|
85b27895e2 | ||
|
|
940b37428c | ||
|
|
eedcf65bd8 | ||
|
|
bf28d2d2d1 | ||
|
|
a57e1f79ab | ||
|
|
6e142bdb74 | ||
|
|
79cb8d8e2e | ||
|
|
1c9d4d8a1e | ||
|
|
6fc32b9bfa | ||
|
|
e0624effc2 | ||
|
|
fe94765c15 | ||
|
|
ffd91e2cf5 | ||
|
|
44e82c0286 | ||
|
|
33d7157b12 | ||
|
|
5c4c6be745 | ||
|
|
5e53066ba7 |
@@ -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
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
# Netbox Topology Views Plugin
|
# Netbox Topology Views Plugin
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
Create topology views/maps from your devices in netbox.
|
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
|
## Preview
|
||||||
|
|
||||||
@@ -9,17 +12,42 @@ Support to filter on name, site and device role.
|
|||||||
|
|
||||||
## Install
|
## 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.
|
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`
|
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
|
### 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.
|
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
|
||||||
|
|
||||||
|
The coordinates can then be provided as: "X;Y"
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
|
|
||||||
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
|
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
|
||||||
@@ -28,16 +56,21 @@ Example:
|
|||||||
```
|
```
|
||||||
PLUGINS_CONFIG = {
|
PLUGINS_CONFIG = {
|
||||||
'netbox_topology_views': {
|
'netbox_topology_views': {
|
||||||
'device_img': 'router,switch,firewall',
|
'device_img': ['router','switch', 'firewall'],
|
||||||
'preselected_device_roles': 'Router, Firewall'
|
'preselected_device_roles': ['Router', 'Firewall']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Setting | Default value | Description |
|
| 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. |
|
| 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. |
|
| 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 |
|
||||||
|
| 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
|
### Custom Images
|
||||||
|
|
||||||
@@ -47,4 +80,22 @@ If you add your own image you also need to add the slug to the `device_img` sett
|
|||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
Go to the plugins tab in the navbar and click topology or go to NETBOX_URL/plugins/topology-views/ view your topologies
|
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/topology-views/` to view your topologies
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
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
|
||||||
+1
-1
@@ -10,7 +10,7 @@ var jsVendorFiles = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
var cssVendorFiles = [
|
var cssVendorFiles = [
|
||||||
'node_modules/vis-network/styles/vis-network.min.css',
|
'node_modules/vis-network/dist/dist/vis-network.min.css',
|
||||||
];
|
];
|
||||||
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
|
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
|
||||||
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
|
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ from extras.plugins import PluginConfig
|
|||||||
class TopologyViewsConfig(PluginConfig):
|
class TopologyViewsConfig(PluginConfig):
|
||||||
name = 'netbox_topology_views'
|
name = 'netbox_topology_views'
|
||||||
verbose_name = 'Topology views'
|
verbose_name = 'Topology views'
|
||||||
description = 'An plugin to render toplogoy maps'
|
description = 'An plugin to render topology maps'
|
||||||
version = '0.3'
|
version = '1.0.0-alpha.5'
|
||||||
author = 'Mattijs Vanhaverbeke'
|
author = 'Mattijs Vanhaverbeke'
|
||||||
author_email = 'author@example.com'
|
author_email = 'author@example.com'
|
||||||
base_url = 'topology-views'
|
base_url = 'netbox_topology_views'
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
'preselected_device_roles': ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'],
|
||||||
'IGNORE_CABLE_TYPE': 'dcim.poweroutlet,dcim.powerport',
|
'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'
|
'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': True,
|
||||||
|
'draw_default_layout': False
|
||||||
}
|
}
|
||||||
|
|
||||||
config = TopologyViewsConfig
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""REST API"""
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
from dcim.models import DeviceRole
|
from dcim.models import DeviceRole, Device
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
class PreDeviceRoleSerializer(ModelSerializer):
|
|
||||||
|
class TopologyDummySerializer(ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = Device
|
||||||
fields = ('id', 'name')
|
fields = ('id', 'name')
|
||||||
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
from . import views
|
||||||
from .views import PreSelectDeviceRolesViewSet, SearchViewSet
|
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
|
|
||||||
router.register('search', SearchViewSet, basename='search')
|
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
@@ -1,89 +1,60 @@
|
|||||||
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet, GenericViewSet
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from .serializers import PreDeviceRoleSerializer
|
from .serializers import TopologyDummySerializer
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from utilities.api import IsAuthenticatedOrLoginNotRequired
|
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Device, Cable
|
from dcim.models import DeviceRole, Device, Cable
|
||||||
|
from circuits.models import Circuit
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
|
class TopologyViewsRootView(APIRootView):
|
||||||
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
def get_view_name(self):
|
||||||
preselected_device_roles = preselected_device_roles_raw.split(",")
|
return 'TopologyViews'
|
||||||
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
|
|
||||||
serializer_class = PreDeviceRoleSerializer
|
|
||||||
|
|
||||||
class SearchViewSet(ViewSet):
|
|
||||||
_ignore_model_permissions = True
|
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
|
||||||
|
|
||||||
def _filter(self, site, role, name):
|
|
||||||
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)
|
|
||||||
return filter_devices
|
|
||||||
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||||
def search(self, request):
|
queryset = Device.objects.all()
|
||||||
name = None
|
serializer_class = TopologyDummySerializer
|
||||||
sites = None
|
|
||||||
devicerole = 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"]
|
|
||||||
|
|
||||||
devices = self._filter(sites, devicerole, name)
|
|
||||||
nodes = []
|
|
||||||
edges = []
|
|
||||||
edge_ids = 0
|
|
||||||
cable_ids = []
|
|
||||||
for device in devices:
|
|
||||||
cables = device.get_cables()
|
|
||||||
for cable in cables:
|
|
||||||
if cable.id not in cable_ids:
|
|
||||||
cable_ids.append(cable.id)
|
|
||||||
edge_ids += 1
|
|
||||||
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.termination_a.device.name + " [" + cable.termination_a.name + "]<br>" + cable.termination_b.device.name + " [" + cable.termination_b.name + "]"
|
|
||||||
edges.append(edge)
|
|
||||||
node = {}
|
|
||||||
node["id"] = device.id
|
|
||||||
node["name"] = device.name
|
|
||||||
node["label"] = device.name + " " + device.device_type.display_name
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
@action(detail=False, methods=['patch'])
|
||||||
|
def save_coords(self, request):
|
||||||
results = {}
|
results = {}
|
||||||
results["nodes"] = nodes
|
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
|
||||||
results["edges"] = edges
|
device_id = None
|
||||||
|
x_coord = None
|
||||||
|
y_coord = None
|
||||||
|
if "node_id" in request.data:
|
||||||
|
if request.data["node_id"]:
|
||||||
|
device_id = request.data["node_id"]
|
||||||
|
if "x" in request.data:
|
||||||
|
if request.data["x"]:
|
||||||
|
x_coord = request.data["x"]
|
||||||
|
if "y" in request.data:
|
||||||
|
if request.data["y"]:
|
||||||
|
y_coord = request.data["y"]
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
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(
|
PluginMenuItem(
|
||||||
link='plugins:netbox_topology_views:home',
|
link='plugins:netbox_topology_views:home',
|
||||||
link_text='Topology',
|
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:!0},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{term: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{term:e.term}},processResults:function(e){return{results:$.map(e.results,function(e){return{text:e.name,id:e.id}})}}}});var n=$("#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);n.append(a).trigger("change"),n.trigger({type:"select2:select",params:{data:t}})})})}function handleButtonPress(){$("#search-form").submit(function(e){$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>'),e.preventDefault();var t=$("#name").val(),a=$("#device-roles").val(),n=$("#sites").val();$.ajax({type:"POST",url:"/api/plugins/topology-views/search/search/",data:JSON.stringify({name:t,devicerole:a,sites:n}),headers:{"X-CSRFToken":csrftoken},contentType:"application/json; charset=utf-8",dataType:"json",success:function(e){$("#status").html('<span class="badge badge-pill badge-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="badge badge-pill badge-success">Ready</span>')}),graph.on("dragEnd",function(e){dragged=this.getPositions(e.nodes),$.each(dragged,function(e,t){$("#coordstatus").html(""),$("#checkSaveCoordinates").is(":checked")&&(nodes.update({id:e,physics:!1}),$.ajax({url:"/api/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="badge badge-pill badge-warning">Failed to update coordinates</span>')},success:function(e){$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>'),$.ajax({url:"/api/reload_devices",type:"GET",headers:{"X-CSRFToken":csrftoken},success:function(e){}})}}))})})},error:function(e){$("#status").html('<span class="badge badge-pill badge-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 {
|
#visgraph {
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
var graph = null;
|
var graph = null;
|
||||||
var container = null;
|
var container = null;
|
||||||
|
var downloadButton = null;
|
||||||
|
var MIME_TYPE = "image/png";
|
||||||
|
var canvas = null;
|
||||||
var csrftoken = null;
|
var csrftoken = null;
|
||||||
var nodes = new vis.DataSet();
|
var nodes = new vis.DataSet();
|
||||||
var edges = new vis.DataSet();
|
var edges = new vis.DataSet();
|
||||||
@@ -7,7 +10,7 @@ var options = {
|
|||||||
interaction: {
|
interaction: {
|
||||||
hover: true,
|
hover: true,
|
||||||
hoverConnectedEdges: true,
|
hoverConnectedEdges: true,
|
||||||
multiselect: true
|
multiselect: false
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
shape: 'image',
|
shape: 'image',
|
||||||
@@ -29,166 +32,51 @@ var options = {
|
|||||||
solver: 'forceAtlas2Based'
|
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() {
|
function iniPlotboxIndex() {
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
container = document.getElementById('visgraph');
|
container = document.getElementById('visgraph');
|
||||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
handleLoadData();
|
||||||
startLoadSearchBar();
|
downloadButton = document.getElementById('btnDownloadImage');
|
||||||
handleButtonPress();
|
btnFullView = document.getElementById('btnFullView');
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLoadSearchBar() {
|
function handleLoadData() {
|
||||||
$('#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 = {
|
|
||||||
term: 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) {
|
|
||||||
|
|
||||||
var queryParameters = {
|
graph = null;
|
||||||
term: params.term
|
nodes = new vis.DataSet();
|
||||||
}
|
edges = new vis.DataSet();
|
||||||
return queryParameters;
|
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||||
},
|
|
||||||
processResults: function (data) {
|
topology_data.edges.forEach(addEdge);
|
||||||
return {
|
topology_data.nodes.forEach(addNode);
|
||||||
results: $.map(data.results, function (item) {
|
|
||||||
return {
|
|
||||||
text: item.name,
|
|
||||||
id: item.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var deviceRolesSelect = $('#device-roles');
|
graph.fit();
|
||||||
$.ajax({
|
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||||
type: 'GET',
|
|
||||||
url: '../../api/plugins/topology-views/preselectdeviceroles/'
|
graph.on('afterDrawing', function () {
|
||||||
}).then(function (data) {
|
var image = canvas.toDataURL(MIME_TYPE);
|
||||||
$.each(data.results, function (index, device_role_to_preload) {
|
downloadButton.href = image;
|
||||||
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
downloadButton.download = "topology";
|
||||||
deviceRolesSelect.append(option).trigger('change');
|
|
||||||
deviceRolesSelect.trigger({
|
|
||||||
type: 'select2:select',
|
|
||||||
params: {
|
|
||||||
data: device_role_to_preload
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleButtonPress() {
|
|
||||||
$("#search-form").submit(function (event) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
|
|
||||||
event.preventDefault();
|
|
||||||
var value = $("#name").val();
|
|
||||||
var value2 = $("#device-roles").val();
|
|
||||||
var value3 = $("#sites").val();
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: "../../api/plugins/topology-views/search/search/",
|
|
||||||
data: JSON.stringify({
|
|
||||||
'name': value,
|
|
||||||
'devicerole': value2,
|
|
||||||
'sites': value3
|
|
||||||
}),
|
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data_result) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-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="badge badge-pill badge-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/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="badge badge-pill badge-warning">Failed to update coordinates</span>');
|
|
||||||
},
|
|
||||||
success: function (data_result) {
|
|
||||||
$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>');
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/reload_devices',
|
|
||||||
type: 'GET',
|
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
success: function (data) {
|
|
||||||
//nothing
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function (error_result) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,74 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load perms %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block title %}Topology Views{% endblock %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
|
||||||
|
{% block head %}
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="row ">
|
{% block controls %}
|
||||||
<div class="col-md-9">
|
<div class="controls">
|
||||||
<div class="panel panel-default">
|
<div class="control-group">
|
||||||
<div class="panel-heading">
|
{% block extra_controls %}{% endblock %}
|
||||||
<strong>Network</strong> <span id="status"></span>
|
<a id="btnDownloadImage" class="btn btn-sm btn-info">
|
||||||
</div>
|
<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 class="panel-body">
|
||||||
<div id="visgraph" class=""></div>
|
<div id="visgraph" class=""></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var topology_data = {{ topology_data | safe }};
|
||||||
|
</script>
|
||||||
</div>
|
</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>
|
||||||
<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">
|
|
||||||
{% 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>
|
|
||||||
<hr class="mb-4">
|
|
||||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||||
|
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<script type="application/javascript"> iniPlotboxIndex()</script>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
|||||||
@@ -1,9 +1,206 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.db.models import Q
|
||||||
from django.views.generic import View
|
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')
|
||||||
|
|
||||||
class TopologyHomeView(View):
|
|
||||||
"""
|
"""
|
||||||
Show the home page
|
Show the home page
|
||||||
"""
|
"""
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return render(request, 'netbox_topology_views/index.html')
|
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
-8
@@ -1,20 +1,27 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "0.2",
|
"version": "1.0.0-alpha.45",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"resources": "gulp build",
|
"resources": "gulp build",
|
||||||
"resources_dev": "gulp build_dev"
|
"resources_dev": "gulp build_dev"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"vis-data": "^7.1.2",
|
||||||
|
"vis-network": "^9.1.0",
|
||||||
|
"vis-util": "^5.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-clean-css": "^4.2.0",
|
"gulp-clean-css": "^4.3.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-sass": "^4.0.2",
|
"gulp-sass": "^4.1.0",
|
||||||
"gulp-uglify": "^3.0.2",
|
"gulp-uglify": "^3.0.2"
|
||||||
"vis-data": "^6.2.3",
|
},
|
||||||
"vis-util": "^1.1.8",
|
"peerDependencies": {
|
||||||
"vis-network": "^6.4.4"
|
"@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(
|
setup(
|
||||||
name='netbox-topology-views',
|
name='netbox-topology-views',
|
||||||
version='0.3',
|
version='1.0.0-alpha.5',
|
||||||
description='An NetBox plugin to create Topology maps',
|
description='An NetBox plugin to create Topology maps',
|
||||||
url='https://github.com/mattieserver/netbox-topology-views',
|
url='https://github.com/mattieserver/netbox-topology-views',
|
||||||
author='Mattijs Vanhaverbeke',
|
author='Mattijs Vanhaverbeke',
|
||||||
|
|||||||
Reference in New Issue
Block a user