Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31a8c756e2 | ||
|
|
ce39c98396 | ||
|
|
faf156a6b2 | ||
|
|
1b36b9e7df | ||
|
|
652a44437e | ||
|
|
76141e70ce | ||
|
|
76b4f6ac9a | ||
|
|
953d7480af | ||
|
|
f323278f00 | ||
|
|
56a6f22bf6 | ||
|
|
b9d3cc3c4e | ||
|
|
d4833071df | ||
|
|
877d308574 | ||
|
|
83b662eac3 | ||
|
|
53a996edbc | ||
|
|
398deb5382 | ||
|
|
b5991a5f50 | ||
|
|
de92627176 | ||
|
|
6be028b9dc | ||
|
|
8997c38263 | ||
|
|
d927e40867 | ||
|
|
62c0dbddb2 | ||
|
|
e68a8a1af0 | ||
|
|
68e8aa0512 | ||
|
|
b792128415 | ||
|
|
f5d9f589bf | ||
|
|
3e5b28f208 | ||
|
|
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 | ||
|
|
a1d155fdbc | ||
|
|
f72ca57ac7 | ||
|
|
8a6f1660f0 | ||
|
|
d608d63207 | ||
|
|
4d161fe6ac | ||
|
|
c6a6893c10 | ||
|
|
b2430d1723 | ||
|
|
19ec0b8f2e | ||
|
|
4989c3b0dd | ||
|
|
cf208d43c0 |
@@ -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,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 }}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# This workflows will upload a Python Package using Twine when a release is created
|
||||||
|
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||||
|
|
||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
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 }}
|
||||||
@@ -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
|
||||||
@@ -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,17 +1,103 @@
|
|||||||
# netbox-topology-views
|
# Netbox Topology Views Plugin
|
||||||
|
|
||||||
## preview
|
 
|
||||||
|
|
||||||

|
Create topology views/maps from your devices in netbox.
|
||||||
|
The connections are based on the cables you created in netbox.
|
||||||
|
Support to filter on name, site, tag and device role.
|
||||||
|
|
||||||
## install
|
## Preview
|
||||||
|
|
||||||
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file.
|

|
||||||
Then run python3 manage.py collectstatic --no-input
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# echo netbox-topology-views >> local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed, the plugin needs to be enabled in your `configuration.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In your configuration.py
|
||||||
|
PLUGINS = ["netbox_topology_views"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `python3 manage.py collectstatic --no-input`
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
|
||||||
|
| netbox version | netbox-topology-views version |
|
||||||
|
| ------------- |-------------|
|
||||||
|
| >= 3.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 |
|
||||||
|
|
||||||
|
### Custom field: coordinates
|
||||||
|
|
||||||
|
There is also support for custom fields.
|
||||||
|
|
||||||
There is also support for custom fiels.
|
|
||||||
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.
|
||||||
|
|
||||||
## use
|
The coordinates can then be provided as: "X;Y"
|
||||||
|
|
||||||
go to /plugins/topology-views/ view your topologies
|
## Configure
|
||||||
|
|
||||||
|
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG = {
|
||||||
|
'netbox_topology_views': {
|
||||||
|
'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|
|
||||||
|
| preselected_intermediate_dev_roles | ['Patch-panel'] | The full name of the device roles you want to display in the global view when using end-to-end connections mode. 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
|
||||||
|
|
||||||
|
You upload you own custom images to the netbox static dir (`static/netbox_topology_views/img/`).
|
||||||
|
These images need to be named after de device role slug and have the .png format/extension.
|
||||||
|
If you add your own image you also need to add the slug to the `device_img` setting.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
+13
@@ -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.
|
||||||
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1 @@
|
|||||||
|
netbox-topology-views
|
||||||
+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,22 @@ 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 = '2.0.2'
|
||||||
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',
|
'preselected_intermediate_dev_roles' : [],
|
||||||
'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'
|
'end2end_connections': False,
|
||||||
|
'ignore_cable_type': ['power outlet','power port'],
|
||||||
|
'device_img': ['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage', 'wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'],
|
||||||
|
'allow_coordinates_saving': False,
|
||||||
|
'preselected_tags' : [],
|
||||||
|
'enable_circuit_terminations': False,
|
||||||
|
'draw_default_layout': False
|
||||||
}
|
}
|
||||||
|
|
||||||
config = TopologyViewsConfig
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
#no models yet
|
|
||||||
@@ -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,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)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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 = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
|
||||||
|
end2end = settings.PLUGINS_CONFIG["netbox_topology_views"]["end2end_connections"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
|
model = Device
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'hide_unconnected', 'save_coords', 'end2end_connections')),
|
||||||
|
(None, ('tenant_group_id', 'tenant_id')),
|
||||||
|
(None, ('region_id', 'site_id', 'location_id')),
|
||||||
|
(None, ('device_role_id', 'intermediate_dev_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')
|
||||||
|
)
|
||||||
|
end2end_connections = forms.BooleanField(
|
||||||
|
label=_("Display end-to-end connections"),
|
||||||
|
required=False,
|
||||||
|
initial=end2end
|
||||||
|
)
|
||||||
|
intermediate_dev_role_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Intermediate Devices Role'),
|
||||||
|
help_text='Intermediate devices to display when using end-to-end connections mode, even if they do not match the query'
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
save_coords = forms.BooleanField(
|
||||||
|
label=_("Save Coordinates"),
|
||||||
|
required=False,
|
||||||
|
disabled=(not allow_coordinates_saving),
|
||||||
|
initial=False
|
||||||
|
)
|
||||||
|
status = MultipleChoiceField(
|
||||||
|
choices=DeviceStatusChoices,
|
||||||
|
required=False,
|
||||||
|
label=_("Device Status")
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# no modeles yet
|
|
||||||
@@ -4,7 +4,6 @@ from utilities.choices import ButtonColorChoices
|
|||||||
menu_items = (
|
menu_items = (
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link='plugins:netbox_topology_views:home',
|
link='plugins:netbox_topology_views:home',
|
||||||
link_text='Topology',
|
link_text='Topology'
|
||||||
buttons=()
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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,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"},shadow:{enabled:!0}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[],coord_save_checkbox=null,htmlElement=null;function getCookie(e){var o=null;if(document.cookie&&""!==document.cookie)for(var t=document.cookie.split(";"),n=0;n<t.length;n++){var d=t[n].trim();if(d.substring(0,e.length+1)===e+"="){o=decodeURIComponent(d.substring(e.length+1));break}}return o}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(){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")},!1)}function performGraphDownload(){var e=document.createElement("a"),o=canvas.toDataURL(MIME_TYPE);e.href=o,e.download="topology",document.body.appendChild(e),e.click(),document.body.removeChild(e)}function handleLoadData(){null!==topology_data&&("dark"==htmlElement.dataset.netboxColorMode&&(options.nodes.font.color="#fff"),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],downloadButton.onclick=function(e){return performGraphDownload(),!1},graph.on("dragEnd",function(e){if(dragged=this.getPositions(e.nodes),coord_save_checkbox.checked&&0!==Object.keys(dragged).length)for(dragged_device in dragged){var o=dragged_device,t=new XMLHttpRequest;t.open("PATCH","/api/plugins/netbox_topology_views/save-coords/save_coords/"),t.setRequestHeader("X-CSRFToken",csrftoken),t.setRequestHeader("Accept","application/json"),t.setRequestHeader("Content-Type","application/json"),t.onreadystatechange=function(){4===t.readyState&&(console.log(t.status),console.log(t.responseText))};var n=JSON.stringify({node_id:o,x:dragged[o].x,y:dragged[o].y});t.send(n)}}),graph.on("doubleClick",function(e){for(selected_device in console.log(e.nodes),selected_devices=e.nodes,selected_devices)console.log(selected_device),console.log(selected_devices[selected_device]),url="/dcim/devices/"+selected_devices[selected_device]+"/",console.log(url),window.open(url,"_blank")}))}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
|||||||
#visgraph {
|
#visgraph {
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-netbox-color-mode=dark] #visgraph {
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
@@ -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,11 +10,11 @@ var options = {
|
|||||||
interaction: {
|
interaction: {
|
||||||
hover: true,
|
hover: true,
|
||||||
hoverConnectedEdges: true,
|
hoverConnectedEdges: true,
|
||||||
multiselect: true
|
multiselect: false
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
shape: 'image',
|
shape: 'image',
|
||||||
brokenImage: '/static/netbox_topology_views/img/role-unknown.png',
|
brokenImage: '../../static/netbox_topology_views/img/role-unknown.png',
|
||||||
size: 35,
|
size: 35,
|
||||||
font: {
|
font: {
|
||||||
multi: 'md',
|
multi: 'md',
|
||||||
@@ -24,171 +27,135 @@ var options = {
|
|||||||
font: {
|
font: {
|
||||||
face: 'helvetica',
|
face: 'helvetica',
|
||||||
},
|
},
|
||||||
|
shadow: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
physics: {
|
physics: {
|
||||||
solver: 'forceAtlas2Based'
|
solver: 'forceAtlas2Based'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var selected_regions = [];
|
||||||
|
var selected_sites = [];
|
||||||
|
var coord_save_checkbox = null;
|
||||||
|
var htmlElement = null;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 () {
|
||||||
|
csrftoken = getCookie('csrftoken');
|
||||||
container = document.getElementById('visgraph');
|
container = document.getElementById('visgraph');
|
||||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
htmlElement = document.getElementsByTagName("html")[0];
|
||||||
startLoadSearchBar();
|
downloadButton = document.getElementById('btnDownloadImage');
|
||||||
handleButtonPress();
|
handleLoadData();
|
||||||
|
btnFullView = document.getElementById('btnFullView');
|
||||||
|
coord_save_checkbox = document.getElementById('id_save_coords');
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLoadSearchBar() {
|
function performGraphDownload() {
|
||||||
$('#device-roles').select2({
|
var tempDownloadLink = document.createElement('a');
|
||||||
allowClear: true,
|
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
|
||||||
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 = {
|
tempDownloadLink.href = generatedImageUrl;
|
||||||
term: params.term
|
tempDownloadLink.download = "topology";
|
||||||
}
|
document.body.appendChild(tempDownloadLink);
|
||||||
return queryParameters;
|
tempDownloadLink.click();
|
||||||
},
|
document.body.removeChild(tempDownloadLink);
|
||||||
processResults: function (data) {
|
|
||||||
return {
|
|
||||||
results: $.map(data.results, function (item) {
|
|
||||||
return {
|
|
||||||
text: item.name,
|
|
||||||
id: item.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonPress() {
|
function handleLoadData() {
|
||||||
$("#search-form").submit(function (event) {
|
if (topology_data !== null) {
|
||||||
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
|
|
||||||
event.preventDefault();
|
if (htmlElement.dataset.netboxColorMode == "dark") {
|
||||||
var value = $("#name").val();
|
options.nodes.font.color = "#fff";
|
||||||
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 () {
|
graph = null;
|
||||||
$("#status").html('<span class="badge badge-pill badge-success">Ready</span>');
|
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.on("dragEnd", function (params) {
|
graph.fit();
|
||||||
dragged = this.getPositions(params.nodes);
|
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||||
$.each(dragged, function (node_id, coordinates) {
|
|
||||||
$("#coordstatus").html('');
|
downloadButton.onclick = function(e) { performGraphDownload(); return false; };
|
||||||
if ($('#checkSaveCoordinates').is(":checked")) {
|
|
||||||
nodes.update({ id: node_id, physics: false });
|
graph.on("dragEnd", function (params) {
|
||||||
$.ajax({
|
dragged = this.getPositions(params.nodes);
|
||||||
url: "/api/save_coords",
|
|
||||||
type: 'POST',
|
if (coord_save_checkbox.checked) {
|
||||||
dataType: 'json',
|
if (Object.keys(dragged).length !== 0) {
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
for (dragged_device in dragged) {
|
||||||
contentType: "application/json; charset=utf-8",
|
var node_id = dragged_device;
|
||||||
data: JSON.stringify({
|
|
||||||
'node_id': node_id,
|
var url = "/api/plugins/netbox_topology_views/save-coords/save_coords/";
|
||||||
'x': coordinates.x,
|
var xhr = new XMLHttpRequest();
|
||||||
'y': coordinates.y
|
xhr.open("PATCH", url);
|
||||||
}),
|
xhr.setRequestHeader('X-CSRFToken', csrftoken );
|
||||||
error: function (error_result) {
|
xhr.setRequestHeader("Accept", "application/json");
|
||||||
$("#coordstatus").html('<span class="badge badge-pill badge-warning">Failed to update coordinates</span>');
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
},
|
|
||||||
success: function (data_result) {
|
xhr.onreadystatechange = function () {
|
||||||
$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>');
|
if (xhr.readyState === 4) {
|
||||||
$.ajax({
|
console.log(xhr.status);
|
||||||
url: '/api/reload_devices',
|
console.log(xhr.responseText);
|
||||||
type: 'GET',
|
}};
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
success: function (data) {
|
var data = JSON.stringify({
|
||||||
//nothing
|
'node_id': node_id,
|
||||||
},
|
'x': dragged[node_id].x,
|
||||||
});
|
'y': dragged[node_id].y});
|
||||||
},
|
|
||||||
});
|
xhr.send(data);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
},
|
|
||||||
error: function (error_result) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
graph.on("doubleClick", function (params) {
|
||||||
|
console.log(params.nodes);
|
||||||
|
selected_devices = params.nodes;
|
||||||
|
for (selected_device in selected_devices) {
|
||||||
|
url = "/dcim/devices/" + selected_devices[selected_device] + "/";
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
{% 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" href="#">
|
||||||
</div>
|
<i class="mdi mdi-download"></i>
|
||||||
<div class="panel-body">
|
Download
|
||||||
<div id="visgraph" class=""></div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
{% endblock controls %}
|
||||||
<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>
|
{% 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 %}
|
||||||
|
|
||||||
<div class="panel panel-default mt-3">
|
{% block content-wrapper %}
|
||||||
<div class="panel-heading">
|
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||||
<strong>Settings</strong>
|
<div class="tab-content">
|
||||||
</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>
|
|
||||||
|
|
||||||
{% endwith %}
|
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||||
{% endblock %}
|
<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 %}
|
{% block javascript %}
|
||||||
|
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
<script type="application/javascript">iniPlotboxIndex()</script>
|
||||||
|
{% endblock javascript %}
|
||||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -1,9 +1,313 @@
|
|||||||
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, PathEndpoint
|
||||||
|
from circuits.models import CircuitTermination, ProviderNetwork
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
|
def create_node(device):
|
||||||
|
dev_name = device.name
|
||||||
|
if dev_name is None:
|
||||||
|
dev_name = "device name unknown"
|
||||||
|
|
||||||
|
node_content = ""
|
||||||
|
|
||||||
|
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>"
|
||||||
|
|
||||||
|
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"] 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
|
||||||
|
return node
|
||||||
|
|
||||||
|
def create_edge(edge_id, cable, termination_a, termination_b, path = None, circuit = None):
|
||||||
|
cable_a_dev_name = "device A name unknown" if termination_a.device.name is None else termination_a.device.name
|
||||||
|
cable_a_name = "device A name unknown" if termination_a.name is None else termination_a.name
|
||||||
|
cable_b_dev_name = "device A name unknown" if termination_b.device.name is None else termination_b.device.name
|
||||||
|
cable_b_name = "cable B name unknown" if termination_b.name is None else termination_b.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 + "]"
|
||||||
|
else:
|
||||||
|
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||||
|
|
||||||
|
if path is not None:
|
||||||
|
edge["title"] += "" if len(path) <= 0 else "<br>Through " + "/".join(path)
|
||||||
|
|
||||||
|
if cable is not None and cable.color != "":
|
||||||
|
edge["color"] = "#" + cable.color
|
||||||
|
|
||||||
|
return edge
|
||||||
|
|
||||||
|
def get_topology_data(queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections):
|
||||||
|
nodes_devices = {}
|
||||||
|
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"]
|
||||||
|
enable_circuit_terminations = settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]
|
||||||
|
|
||||||
|
device_ids = [d.id for d in queryset]
|
||||||
|
|
||||||
|
links = Cable.objects.filter( Q(_termination_a_device_id__in=device_ids) | Q(_termination_b_device_id__in=device_ids) ) \
|
||||||
|
.select_related("termination_a_type", "termination_b_type") \
|
||||||
|
.prefetch_related("termination_a", "termination_b")
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
|
||||||
|
if link.termination_a_type.name in ignore_cable_type or link.termination_b_type.name in ignore_cable_type \
|
||||||
|
or link.id in cable_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
a_is_path_endoint = isinstance(link.termination_a, PathEndpoint)
|
||||||
|
b_is_path_endoint = isinstance(link.termination_b, PathEndpoint)
|
||||||
|
|
||||||
|
if not end2end_connections or (not a_is_path_endoint and not b_is_path_endoint):
|
||||||
|
|
||||||
|
if isinstance(link.termination_a, CircuitTermination):
|
||||||
|
if enable_circuit_terminations and link.termination_a.circuit.id not in circuit_ids:
|
||||||
|
circuit = link.termination_a.circuit
|
||||||
|
circuit_ids.append(circuit.id)
|
||||||
|
cable_ids.append(link.id)
|
||||||
|
|
||||||
|
path_destination = circuit.termination_z if link.termination_a.term_side == "A" else circuit.termination_a
|
||||||
|
|
||||||
|
if path_destination is not None and path_destination.provider_network is None:
|
||||||
|
# ProviderNetwork not supported at the moment
|
||||||
|
# $path_destination.cable would be none : there is no cable between a CircuitTermination and ProviderNetwork
|
||||||
|
|
||||||
|
if not hasattr(link.termination_b, 'device'):
|
||||||
|
#CircuitTermination B Missing Device
|
||||||
|
continue
|
||||||
|
|
||||||
|
origin_device = link.termination_b.device
|
||||||
|
destination_device = path_destination.cable.termination_b.device
|
||||||
|
|
||||||
|
if origin_device.id in device_ids and destination_device.id in device_ids:
|
||||||
|
if origin_device.id not in nodes_devices:
|
||||||
|
nodes_devices[origin_device.id] = origin_device
|
||||||
|
if destination_device.id not in nodes_devices:
|
||||||
|
nodes_devices[destination_device.id] = destination_device
|
||||||
|
|
||||||
|
edge_ids += 1
|
||||||
|
edges.append(create_edge(edge_ids, link, link.termination_b, path_destination.cable.termination_b, [circuit.cid], circuit))
|
||||||
|
|
||||||
|
elif link.termination_a.device.id in device_ids and link.termination_b.device.id in device_ids:
|
||||||
|
|
||||||
|
if link.termination_a.device.id not in nodes_devices:
|
||||||
|
nodes_devices[link.termination_a.device.id] = link.termination_a.device
|
||||||
|
if link.termination_b.device.id not in nodes_devices:
|
||||||
|
nodes_devices[link.termination_b.device.id] = link.termination_b.device
|
||||||
|
|
||||||
|
cable_ids.append(link.id)
|
||||||
|
edge_ids += 1
|
||||||
|
edges.append(create_edge(edge_ids, link, link.termination_a, link.termination_b))
|
||||||
|
else:
|
||||||
|
# termination_a can be a CircuitTermination (no trace()) while termination_b is a PathEndpoint
|
||||||
|
# If so, we swap them so we can follow the path using PathEndpoint#trace()
|
||||||
|
path_start = link.termination_a if a_is_path_endoint else link.termination_b
|
||||||
|
if path_start.device is not None and path_start.device.id not in device_ids:
|
||||||
|
# device not in queryset, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ignore incomplete path when in end-to-end connection mode
|
||||||
|
if path_start.path is not None:
|
||||||
|
if not path_start.path.is_active or path_start.path.is_split:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
path_destination = path_start.path.destination
|
||||||
|
if hasattr(path_destination, "device") and path_destination.device is not None and path_destination.device.id not in device_ids:
|
||||||
|
# device not in queryset, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_path = False
|
||||||
|
|
||||||
|
# variables needed for trace() browsing
|
||||||
|
origin = None
|
||||||
|
circuit = None
|
||||||
|
tmp_path = []
|
||||||
|
|
||||||
|
# trace() : Return the path as a list of three-tuples (A termination, cable, B termination)
|
||||||
|
# TODO device qui ne s"affichent pas si hide_unconnected
|
||||||
|
for (termination_a, cable, termination_b) in path_start.trace():
|
||||||
|
|
||||||
|
if origin is None:
|
||||||
|
# New part of the link
|
||||||
|
origin = termination_a
|
||||||
|
circuit = None
|
||||||
|
tmp_path = []
|
||||||
|
|
||||||
|
if isinstance(termination_b, ProviderNetwork):
|
||||||
|
# ProviderNetwork not supported at the moment
|
||||||
|
# It would need to manage several kind of nodes (Device, ProviderNetwork)
|
||||||
|
# and maps javascript nodes ID with Devices IDs and ProviderNetworks IDs
|
||||||
|
# When $termination_b is a ProviderNetwork instance, $cable will be None
|
||||||
|
break
|
||||||
|
|
||||||
|
if cable is not None and (cable.termination_a_type in ignore_cable_type or cable.termination_b_type in ignore_cable_type):
|
||||||
|
# Ignore this path
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(termination_b, CircuitTermination):
|
||||||
|
if enable_circuit_terminations:
|
||||||
|
circuit = termination_b.circuit
|
||||||
|
tmp_path.append(circuit.cid)
|
||||||
|
if circuit.id not in circuit_ids:
|
||||||
|
circuit_ids.append(circuit.id)
|
||||||
|
else:
|
||||||
|
# Ignore this path
|
||||||
|
break
|
||||||
|
elif end2end_connections and termination_b != path_destination \
|
||||||
|
and termination_b.device.device_role.id not in intermediate_dev_role_ids \
|
||||||
|
and termination_b.device.id not in device_ids:
|
||||||
|
# Skip this intermediate device, origin device is keep in $origin for next iteration
|
||||||
|
tmp_path.append(termination_b.device.name)
|
||||||
|
elif cable is not None and termination_b is not None:
|
||||||
|
if cable.id not in cable_ids:
|
||||||
|
# New part of the link we want to display
|
||||||
|
cable_ids.append(cable.id)
|
||||||
|
edge_ids += 1
|
||||||
|
edges.append(create_edge(edge_ids, cable, origin, termination_b, tmp_path, circuit))
|
||||||
|
|
||||||
|
if not valid_path:
|
||||||
|
valid_path = True
|
||||||
|
|
||||||
|
if termination_b.device.id not in nodes_devices and \
|
||||||
|
(termination_b.device.device_role.id in intermediate_dev_role_ids or termination_b.device.id in device_ids) :
|
||||||
|
nodes_devices[termination_b.device.id] = termination_b.device
|
||||||
|
# Reset for next iteration
|
||||||
|
origin = None
|
||||||
|
# endfor (trace)
|
||||||
|
|
||||||
|
if valid_path and path_start.device.id not in nodes_devices:
|
||||||
|
nodes_devices[path_start.device.id] = path_start.device
|
||||||
|
|
||||||
|
# endfor (link)
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
results["nodes"] = [create_node(d) for d in nodes_devices.values()]
|
||||||
|
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().select_related("device_type", "device_role")
|
||||||
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
|
topo_data = None
|
||||||
|
|
||||||
|
if request.GET:
|
||||||
|
hide_unconnected = False
|
||||||
|
if "hide_unconnected" in request.GET:
|
||||||
|
if request.GET["hide_unconnected"] == "on" :
|
||||||
|
hide_unconnected = True
|
||||||
|
|
||||||
|
if "intermediate_dev_role_id" in request.GET:
|
||||||
|
intermediate_dev_role_ids = list(map(int, request.GET.getlist("intermediate_dev_role_id")))
|
||||||
|
else:
|
||||||
|
intermediate_dev_role_ids = []
|
||||||
|
|
||||||
|
end2end_connections = False
|
||||||
|
if "end2end_connections" in request.GET:
|
||||||
|
if request.GET["end2end_connections"] == "on" :
|
||||||
|
end2end_connections = True
|
||||||
|
|
||||||
|
if "draw_init" in request.GET:
|
||||||
|
if request.GET["draw_init"].lower() == "true":
|
||||||
|
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
|
||||||
|
else:
|
||||||
|
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
|
||||||
|
else:
|
||||||
|
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
||||||
|
preselected_intermediate_dev_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_intermediate_dev_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_intermediate_dev_role_id = DeviceRole.objects.filter(name__in=preselected_intermediate_dev_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("intermediate_dev_role_id", list(q_intermediate_dev_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
+955
-292
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": "2.0.2",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
from setuptools import setup, find_packages
|
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(
|
setup(
|
||||||
name='netbox-topology-views',
|
name='netbox-topology-views',
|
||||||
version='0.3',
|
version='2.0.2',
|
||||||
description='An NetBox plugin to create Topology maps',
|
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',
|
url='https://github.com/mattieserver/netbox-topology-views',
|
||||||
author='Mattijs Vanhaverbeke',
|
author='Mattijs Vanhaverbeke',
|
||||||
license='Apache 2.0',
|
license='Apache 2.0',
|
||||||
install_requires=[],
|
install_requires=[],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
keywords=['netbox-plugin'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
|
|||||||
Reference in New Issue
Block a user