Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d082af4a9b | ||
|
|
2501e41b7c | ||
|
|
aa41f8a402 | ||
|
|
6c512e0e9b | ||
|
|
5b96496b46 | ||
|
|
88edcde8d6 | ||
|
|
7f18a3ad6f | ||
|
|
1925be271a | ||
|
|
a53486ca23 | ||
|
|
4bc418a5d0 | ||
|
|
e952111a53 | ||
|
|
e73cb5fa1a | ||
|
|
56552bdd02 | ||
|
|
f914e78812 | ||
|
|
c9a7b2315e | ||
|
|
a8c9fc5fbe | ||
|
|
a1f6e3dc92 | ||
|
|
4f8053c0f6 | ||
|
|
3b3a6d63d4 | ||
|
|
ce283dea52 | ||
|
|
f3579cce63 | ||
|
|
3bb711ea84 | ||
|
|
9d76bb421d | ||
|
|
7a24221444 | ||
|
|
ac198ef7cf | ||
|
|
6d05b1b6ca | ||
|
|
049fb045c0 | ||
|
|
9e81b94d31 | ||
|
|
18fed2c029 | ||
|
|
a02acbc561 | ||
|
|
74d648b94d | ||
|
|
e9422d70e3 | ||
|
|
15b61fd7f9 | ||
|
|
c264536850 | ||
|
|
2dbcaa4877 | ||
|
|
6884bb3b94 | ||
|
|
bf47320864 | ||
|
|
f7bb83c913 | ||
|
|
0519796af6 | ||
|
|
adea4437c9 |
@@ -0,0 +1,3 @@
|
||||
COMPOSE_PATH_SEPARATOR=:
|
||||
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME=netbox-docker
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build the Docker image
|
||||
run: docker compose build --no-cache
|
||||
|
||||
- name: Verify the Docker image
|
||||
# clearcache is provided by Django extras, which is not loaded if netbox-slm cannot be loaded
|
||||
run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py clearcache"
|
||||
|
||||
- name: Run Django tests with coverage for netbox_slm
|
||||
run: |
|
||||
chmod go+w ci/reports
|
||||
docker compose run netbox sh -c " \
|
||||
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm && \
|
||||
/opt/netbox/venv/bin/coverage report --fail-under=0 && \
|
||||
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
|
||||
|
||||
- name: Sonarcloud scan
|
||||
if: env.SONAR_TOKEN != null
|
||||
uses: sonarsource/sonarcloud-github-action@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install tools
|
||||
run: pip install .[tools]
|
||||
|
||||
- name: Run ruff
|
||||
run: ruff netbox_slm
|
||||
|
||||
- name: Run black
|
||||
run: black netbox_slm
|
||||
@@ -1,3 +1,4 @@
|
||||
**/netbox
|
||||
**/netbox-docker
|
||||
**/*.pyc
|
||||
**/__pycache__
|
||||
@@ -5,3 +6,8 @@
|
||||
**/.vscode
|
||||
**/dist
|
||||
**/netbox_slm.egg-info
|
||||
ci/docker-compose.override.yml
|
||||
ci/reports
|
||||
out/production
|
||||
*.iml
|
||||
build/
|
||||
|
||||
@@ -4,10 +4,10 @@ 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
|
||||
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.
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
include ../LICENSE
|
||||
include ../README.md
|
||||
include ../SECURITY.md
|
||||
recursive-include netbox_slm/templates *
|
||||
@@ -1,12 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
twine = "3.7.1"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
Generated
-192
@@ -1,192 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f3cebe84c169ba6773cfcfae65b47e5076fa331e445f47a6ec4d13306942deb4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
|
||||
"sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
|
||||
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.10"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
|
||||
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.18.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
|
||||
"sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.10.0"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
|
||||
"sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.5.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
|
||||
"sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
|
||||
],
|
||||
"version": "==1.8.2"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4",
|
||||
"sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.11.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
|
||||
"sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.6"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
|
||||
"sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==32.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5",
|
||||
"sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.27.0"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c",
|
||||
"sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.62.3"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
|
||||
"sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.7"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
|
||||
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
-66
@@ -1,41 +1,48 @@
|
||||
Software Lifecycle Management
|
||||
-----------------------------
|
||||
<h1 align="center">NetBox SLM</h1>
|
||||
|
||||
*Netbox Plugin for lifecycle management of software components, including versions and installations*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
<p align="center"><i>Netbox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
|
||||
|
||||
--------------
|
||||
<div align="center">
|
||||
<a href="https://pypi.org/project/netbox_slm/"><img src="https://img.shields.io/pypi/v/netbox_slm" alt="PyPi"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/stargazers"><img src="https://img.shields.io/github/stars/ICTU/netbox_slm" alt="Stars Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/network/members"><img src="https://img.shields.io/github/forks/ICTU/netbox_slm" alt="Forks Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/pulls"><img src="https://img.shields.io/github/issues-pr/ICTU/netbox_slm" alt="Pull Requests Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/issues"><img src="https://img.shields.io/github/issues/ICTU/netbox_slm" alt="Issues Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/graphs/contributors"><img alt="GitHub contributors" src="https://img.shields.io/github/contributors/ICTU/netbox_slm?color=2b9348"></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ICTU/netbox_slm?color=2b9348" alt="License Badge"/></a>
|
||||
</div>
|
||||
|
||||
Installation Guide
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using the Docker version of Netbox, first follow the `netbox-docker
|
||||
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
|
||||
instructions to clone the netbox-docker repo and set up the
|
||||
``docker-compose.override.yml``.
|
||||
## Known Issues
|
||||
|
||||
Next, follow these instructions (based on the generic `Netbox plugin
|
||||
instructions <https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins>`__)
|
||||
- WARNING: This plugin is only tested with a single Netbox version at this time.
|
||||
- CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
|
||||
|
||||
## Installation Guide
|
||||
|
||||
When using the Docker version of Netbox, first follow the netbox-docker [quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the netbox-docker repo and set up the ``docker-compose.override.yml``.
|
||||
|
||||
Next, follow these instructions (based on the Netbox docker variant
|
||||
[instructions](https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files))
|
||||
to install the Netbox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/configuration.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.93`` as
|
||||
``configuration/plugins.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
|
||||
```
|
||||
FROM netboxcommunity/netbox:vX.Y.Z
|
||||
|
||||
COPY ./plugin_requirements.txt /
|
||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
||||
```
|
||||
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
version: '3.4'
|
||||
```
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
@@ -48,112 +55,104 @@ to install the Netbox SLM plugin:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
```
|
||||
|
||||
Now, build the image: ``docker compose build --no-cache``
|
||||
|
||||
And finally, run Netbox with the SLM plugin: ``docker compose up``
|
||||
|
||||
Releasing Guide
|
||||
~~~~~~~~~~~~~~~
|
||||
## Releasing Guide
|
||||
|
||||
To draft a release;
|
||||
|
||||
update the setup.py file to reflect the new version, then from the *src*
|
||||
update the `netbox_slm/__init__.py` file to reflect the new version, then from the *src*
|
||||
directory run
|
||||
|
||||
::
|
||||
|
||||
# make sure to update the version in netbox_slm/__init__.py and setup.cfg
|
||||
$ python setup.py sdist
|
||||
```
|
||||
$ python -m build
|
||||
$ twine upload dist/*
|
||||
```
|
||||
|
||||
On Github.com create a similar tag and version. These steps could be
|
||||
automated with a github workflow.
|
||||
|
||||
n.b. Currently the plugin is configured to use a personal pypi account,
|
||||
this should be changed.
|
||||
|
||||
Developer Guide (local installation)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
## Developer Guide (local installation)
|
||||
|
||||
*Follow the steps below on your local system to run netbox and the
|
||||
netbox_slm plugin in developer mode*
|
||||
|
||||
Setup
|
||||
~~~~~
|
||||
### Setup
|
||||
|
||||
The goal below is to run all Netbox components in Docker and run a local
|
||||
Netbox Django copy with auto-reload to develop the plugin pointing to
|
||||
the Dockerized postgres and redis instances, basically ignoring the
|
||||
netbox docker runtime server.
|
||||
|
||||
Gotcha’s
|
||||
~~~~~~~~
|
||||
|
||||
*Netbox’s exception handler seems to supress initial Exceptions,
|
||||
obfuscating missing imports or other Django related issues by re-raising
|
||||
an opague error, e.g. netbox_slm plugin namespace doesnotexist. It’s
|
||||
recommended to often check the runtime server or patch Netbox’s global
|
||||
exception handler.*
|
||||
|
||||
Steps
|
||||
~~~~~
|
||||
|
||||
::
|
||||
### Steps
|
||||
|
||||
from your projects directory clone the netbox repository
|
||||
|
||||
```
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
$ git checkout f5356b8 (to pin 3.0.9)
|
||||
|
||||
```
|
||||
|
||||
install the virtual environment
|
||||
|
||||
|
||||
```
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
```
|
||||
|
||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
||||
|
||||
|
||||
```
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
```
|
||||
|
||||
The Netbox installation above will be used to run Django management
|
||||
commands like runserver, makemigrations and migrate, which will be
|
||||
explained in the next steps below;
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox-slm repository
|
||||
|
||||
```
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
|
||||
```
|
||||
|
||||
This will start Netbox locally (requires Docker) and forward the redis
|
||||
and postgres ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
|
||||
::
|
||||
|
||||
```
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
|
||||
```
|
||||
|
||||
or stop the stack with
|
||||
|
||||
```
|
||||
$ docker-compose down
|
||||
|
||||
```
|
||||
|
||||
# to start fresh:
|
||||
|
||||
```
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
|
||||
```
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and
|
||||
@@ -161,8 +160,7 @@ redis connection strings (username, password) to the ones the netbox
|
||||
docker backend is using, for example (using default user and passwords
|
||||
from the netbox docker example):
|
||||
|
||||
::
|
||||
|
||||
```
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
@@ -209,15 +207,16 @@ from the netbox docker example):
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
```
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
::
|
||||
|
||||
```
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox-slm/src/
|
||||
$ export PYTHONPATH=../../netbox_slm/netbox_slm/ # or with the pipenv activated run `python3 setup.py develop` from the netbox_slm directory
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
```
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
||||
Visit http://127.0.0.1:8001 in the browser to see the auto reloading
|
||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
Software Lifecycle Management
|
||||
-----------------------------
|
||||
|
||||
*Netbox Plugin for lifecycle management of software components, including versions and installations*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
--------------
|
||||
|
||||
Installation Guide
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using the Docker version of Netbox, first follow the `netbox-docker
|
||||
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
|
||||
instructions to clone the netbox-docker repo and set up the
|
||||
``docker-compose.override.yml``.
|
||||
|
||||
Next, follow these instructions (based on the generic `Netbox plugin
|
||||
instructions <https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins>`__)
|
||||
to install the Netbox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/configuration.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.93`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
|
||||
|
||||
COPY ./plugin_requirements.txt /
|
||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
||||
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
version: '3.4'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- 8000:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-SLM
|
||||
image: netbox:slm
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
|
||||
Now, build the image: ``docker compose build --no-cache``
|
||||
|
||||
And finally, run Netbox with the SLM plugin: ``docker compose up``
|
||||
|
||||
Releasing Guide
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
To draft a release;
|
||||
|
||||
update the setup.py file to reflect the new version, then from the *src*
|
||||
directory run
|
||||
|
||||
::
|
||||
|
||||
# make sure to update the version in netbox_slm/__init__.py
|
||||
$ python setup.py sdist
|
||||
$ twine upload dist/*
|
||||
|
||||
On Github.com create a similar tag and version. These steps could be
|
||||
automated with a github workflow.
|
||||
|
||||
n.b. Currently the plugin is configured to use a personal pypi account,
|
||||
this should be changed.
|
||||
|
||||
Developer Guide (local installation)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Follow the steps below on your local system to run netbox and the
|
||||
netbox_slm plugin in developer mode*
|
||||
|
||||
Setup
|
||||
~~~~~
|
||||
|
||||
The goal below is to run all Netbox components in Docker and run a local
|
||||
Netbox Django copy with auto-reload to develop the plugin pointing to
|
||||
the Dockerized postgres and redis instances, basically ignoring the
|
||||
netbox docker runtime server.
|
||||
|
||||
Gotcha’s
|
||||
~~~~~~~~
|
||||
|
||||
*Netbox’s exception handler seems to supress initial Exceptions,
|
||||
obfuscating missing imports or other Django related issues by re-raising
|
||||
an opague error, e.g. netbox_slm plugin namespace doesnotexist. It’s
|
||||
recommended to often check the runtime server or patch Netbox’s global
|
||||
exception handler.*
|
||||
|
||||
Steps
|
||||
~~~~~
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox repository
|
||||
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
$ git checkout f5356b8 (to pin 3.0.9)
|
||||
|
||||
install the virtual environment
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
|
||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
||||
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
|
||||
The Netbox installation above will be used to run Django management
|
||||
commands like runserver, makemigrations and migrate, which will be
|
||||
explained in the next steps below;
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox-slm repository
|
||||
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
|
||||
This will start Netbox locally (requires Docker) and forward the redis
|
||||
and postgres ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
|
||||
or stop the stack with
|
||||
|
||||
$ docker-compose down
|
||||
|
||||
# to start fresh:
|
||||
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and
|
||||
redis connection strings (username, password) to the ones the netbox
|
||||
docker backend is using, for example (using default user and passwords
|
||||
from the netbox docker example):
|
||||
|
||||
::
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox-slm/src/
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Current status
|
||||
|
||||
Netbox SLM is a plugin for Netbox with mostly default configuration. We advise to monitor Netbox security findings and update accordingly.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version of Netbox SLM is currently being supported with security updates. We aim to keep the plugin compatible with the latest Netbox version, but have no incentive to patch older tags at this point.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities by opening a ticket. The aim is to get back to you within 24 hours with a confirmation of the issue and a brief action plan or a request for more information.
|
||||
@@ -0,0 +1,17 @@
|
||||
ARG PYTHON_VERSION=3.10
|
||||
FROM python:${PYTHON_VERSION}-alpine AS compile-image
|
||||
|
||||
WORKDIR /build
|
||||
RUN python -m venv /opt/netbox/venv
|
||||
ENV PATH="/opt/netbox/venv/bin:$PATH"
|
||||
|
||||
COPY ../ /build
|
||||
|
||||
RUN pip install -U .[build]
|
||||
RUN python -m build
|
||||
RUN pip install --no-index /build
|
||||
|
||||
FROM netboxcommunity/netbox:v3.6.1
|
||||
|
||||
ARG PYTHON_VERSION=3.10
|
||||
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM netboxcommunity/netbox:v3.6.1
|
||||
|
||||
RUN mkdir /ci && chmod go+w /ci
|
||||
COPY ../ci/requirements_ci.txt /ci/
|
||||
RUN pip install -r /ci/requirements_ci.txt
|
||||
|
||||
COPY ../netbox_slm /opt/netbox/netbox/netbox_slm
|
||||
@@ -0,0 +1,257 @@
|
||||
####
|
||||
## We recommend to not edit this file.
|
||||
## Create separate files to overwrite the settings.
|
||||
## See `extra.py` as an example.
|
||||
####
|
||||
|
||||
import re
|
||||
from os import environ
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
# For reference see https://netbox.readthedocs.io/en/stable/configuration/
|
||||
# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
|
||||
|
||||
# Read secret from file
|
||||
def _read_secret(secret_name, default = None):
|
||||
try:
|
||||
f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8')
|
||||
except EnvironmentError:
|
||||
return default
|
||||
else:
|
||||
with f:
|
||||
return f.readline().strip()
|
||||
|
||||
_BASE_DIR = dirname(dirname(abspath(__file__)))
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Required settings #
|
||||
# #
|
||||
#########################
|
||||
|
||||
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
|
||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||
#
|
||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||
ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ')
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': environ.get('DB_NAME', 'netbox'), # Database name
|
||||
'USER': environ.get('DB_USER', ''), # PostgreSQL username
|
||||
'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')),
|
||||
# PostgreSQL password
|
||||
'HOST': environ.get('DB_HOST', 'localhost'), # Database server
|
||||
'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default)
|
||||
'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')},
|
||||
# Database connection SSLMODE
|
||||
'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')),
|
||||
# Max database connection age
|
||||
'DISABLE_SERVER_SIDE_CURSORS': environ.get('DB_DISABLE_SERVER_SIDE_CURSORS', 'False').lower() == 'true',
|
||||
# Disable the use of server-side cursors transaction pooling
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': environ.get('REDIS_HOST', 'localhost'),
|
||||
'PORT': int(environ.get('REDIS_PORT', 6379)),
|
||||
'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')),
|
||||
'DATABASE': int(environ.get('REDIS_DATABASE', 0)),
|
||||
'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true',
|
||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False').lower() == 'true',
|
||||
},
|
||||
'caching': {
|
||||
'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')),
|
||||
'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))),
|
||||
'PASSWORD': _read_secret('redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', ''))),
|
||||
'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)),
|
||||
'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true',
|
||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False')).lower() == 'true',
|
||||
},
|
||||
}
|
||||
|
||||
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
||||
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
||||
# symbols. NetBox will not run without this defined. For more information, see
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
|
||||
SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', ''))
|
||||
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Optional settings #
|
||||
# #
|
||||
#########################
|
||||
|
||||
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
|
||||
# application errors (assuming correct email settings are provided).
|
||||
ADMINS = [
|
||||
# ['John Doe', 'jdoe@example.com'],
|
||||
]
|
||||
|
||||
# URL schemes that are allowed within links in NetBox
|
||||
ALLOWED_URL_SCHEMES = (
|
||||
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
|
||||
)
|
||||
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = environ.get('BANNER_TOP', '')
|
||||
BANNER_BOTTOM = environ.get('BANNER_BOTTOM', '')
|
||||
|
||||
# Text to include on the login page above the login form. HTML is allowed.
|
||||
BANNER_LOGIN = environ.get('BANNER_LOGIN', '')
|
||||
|
||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = environ.get('BASE_PATH', '')
|
||||
|
||||
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
|
||||
CHANGELOG_RETENTION = int(environ.get('CHANGELOG_RETENTION', 90))
|
||||
|
||||
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
|
||||
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
|
||||
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
|
||||
CORS_ORIGIN_ALLOW_ALL = environ.get('CORS_ORIGIN_ALLOW_ALL', 'False').lower() == 'true'
|
||||
CORS_ORIGIN_WHITELIST = list(filter(None, environ.get('CORS_ORIGIN_WHITELIST', 'https://localhost').split(' ')))
|
||||
CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in list(filter(None, environ.get('CORS_ORIGIN_REGEX_WHITELIST', '').split(' ')))]
|
||||
|
||||
# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag.
|
||||
# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like:
|
||||
# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev
|
||||
CSRF_TRUSTED_ORIGINS = list(filter(None, environ.get('CSRF_TRUSTED_ORIGINS', '').split(' ')))
|
||||
|
||||
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
|
||||
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
|
||||
# on a production system.
|
||||
DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
|
||||
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
|
||||
|
||||
# Email settings
|
||||
EMAIL = {
|
||||
'SERVER': environ.get('EMAIL_SERVER', 'localhost'),
|
||||
'PORT': int(environ.get('EMAIL_PORT', 25)),
|
||||
'USERNAME': environ.get('EMAIL_USERNAME', ''),
|
||||
'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')),
|
||||
'USE_SSL': environ.get('EMAIL_USE_SSL', 'False').lower() == 'true',
|
||||
'USE_TLS': environ.get('EMAIL_USE_TLS', 'False').lower() == 'true',
|
||||
'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''),
|
||||
'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''),
|
||||
'TIMEOUT': int(environ.get('EMAIL_TIMEOUT', 10)), # seconds
|
||||
'FROM_EMAIL': environ.get('EMAIL_FROM', ''),
|
||||
}
|
||||
|
||||
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
|
||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||
ENFORCE_GLOBAL_UNIQUE = environ.get('ENFORCE_GLOBAL_UNIQUE', 'False').lower() == 'true'
|
||||
|
||||
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
|
||||
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
|
||||
EXEMPT_VIEW_PERMISSIONS = list(filter(None, environ.get('EXEMPT_VIEW_PERMISSIONS', '').split(' ')))
|
||||
|
||||
# Enable GraphQL API.
|
||||
GRAPHQL_ENABLED = environ.get('GRAPHQL_ENABLED', 'True').lower() == 'true'
|
||||
|
||||
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
|
||||
# https://docs.djangoproject.com/en/stable/topics/logging/
|
||||
LOGGING = {}
|
||||
|
||||
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = environ.get('LOGIN_REQUIRED', 'False').lower() == 'true'
|
||||
|
||||
# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to
|
||||
# re-authenticate. (Default: 1209600 [14 days])
|
||||
LOGIN_TIMEOUT = int(environ.get('LOGIN_TIMEOUT', 1209600))
|
||||
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = environ.get('MAINTENANCE_MODE', 'False').lower() == 'true'
|
||||
|
||||
# Maps provider
|
||||
MAPS_URL = environ.get('MAPS_URL', None)
|
||||
|
||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
||||
# all objects by specifying "?limit=0".
|
||||
MAX_PAGE_SIZE = int(environ.get('MAX_PAGE_SIZE', 1000))
|
||||
|
||||
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
|
||||
# the default value of this setting is derived from the installed location.
|
||||
MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media'))
|
||||
|
||||
# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
|
||||
METRICS_ENABLED = environ.get('METRICS_ENABLED', 'False').lower() == 'true'
|
||||
|
||||
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
|
||||
NAPALM_USERNAME = environ.get('NAPALM_USERNAME', '')
|
||||
NAPALM_PASSWORD = _read_secret('napalm_password', environ.get('NAPALM_PASSWORD', ''))
|
||||
|
||||
# NAPALM timeout (in seconds). (Default: 30)
|
||||
NAPALM_TIMEOUT = int(environ.get('NAPALM_TIMEOUT', 30))
|
||||
|
||||
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
|
||||
# be provided as a dictionary.
|
||||
NAPALM_ARGS = {}
|
||||
|
||||
# Determine how many objects to display per page within a list. (Default: 50)
|
||||
PAGINATE_COUNT = int(environ.get('PAGINATE_COUNT', 50))
|
||||
|
||||
# Enable installed plugins. Add the name of each plugin to the list.
|
||||
PLUGINS = ['netbox_slm']
|
||||
|
||||
# Plugins configuration settings. These settings are used by various plugins that the user may have installed.
|
||||
# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
|
||||
PLUGINS_CONFIG = {
|
||||
}
|
||||
|
||||
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = environ.get('PREFER_IPV4', 'False').lower() == 'true'
|
||||
|
||||
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22))
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220))
|
||||
|
||||
# Remote authentication support
|
||||
REMOTE_AUTH_ENABLED = environ.get('REMOTE_AUTH_ENABLED', 'False').lower() == 'true'
|
||||
REMOTE_AUTH_BACKEND = environ.get('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
||||
REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
|
||||
REMOTE_AUTH_AUTO_CREATE_USER = environ.get('REMOTE_AUTH_AUTO_CREATE_USER', 'True').lower() == 'true'
|
||||
REMOTE_AUTH_DEFAULT_GROUPS = list(filter(None, environ.get('REMOTE_AUTH_DEFAULT_GROUPS', '').split(' ')))
|
||||
|
||||
# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the
|
||||
# version check or use the URL below to check for release in the official NetBox repository.
|
||||
# https://api.github.com/repos/netbox-community/netbox/releases
|
||||
RELEASE_CHECK_URL = environ.get('RELEASE_CHECK_URL', None)
|
||||
|
||||
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
||||
# this setting is derived from the installed location.
|
||||
REPORTS_ROOT = environ.get('REPORTS_ROOT', '/etc/netbox/reports')
|
||||
|
||||
# Maximum execution time for background tasks, in seconds.
|
||||
RQ_DEFAULT_TIMEOUT = int(environ.get('RQ_DEFAULT_TIMEOUT', 300))
|
||||
|
||||
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
||||
# this setting is derived from the installed location.
|
||||
SCRIPTS_ROOT = environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts')
|
||||
|
||||
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
|
||||
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
|
||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||
SESSION_FILE_PATH = environ.get('SESSIONS_ROOT', None)
|
||||
|
||||
# Time zone (default: UTC)
|
||||
TIME_ZONE = environ.get('TIME_ZONE', 'UTC')
|
||||
|
||||
# Date/time formatting. See the following link for supported formats:
|
||||
# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
|
||||
DATE_FORMAT = environ.get('DATE_FORMAT', 'N j, Y')
|
||||
SHORT_DATE_FORMAT = environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
TIME_FORMAT = environ.get('TIME_FORMAT', 'g:i a')
|
||||
SHORT_TIME_FORMAT = environ.get('SHORT_TIME_FORMAT', 'H:i:s')
|
||||
DATETIME_FORMAT = environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
SHORT_DATETIME_FORMAT = environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
@@ -0,0 +1,11 @@
|
||||
PLUGINS = ['netbox_slm']
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS_CONFIG = {
|
||||
'netbox_slm': dict(
|
||||
TEST_RUNNER='xmlrunner.extra.djangotestrunner.XMLTestRunner',
|
||||
TEST_OUTPUT_DIR='/ci/reports/',
|
||||
TEST_OUTPUT_FILE_NAME='junit.xml',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- "8001:8080"
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ci/Dockerfile-CI
|
||||
image: netbox:slm
|
||||
environment:
|
||||
- COVERAGE_FILE=/ci/.coverage
|
||||
volumes:
|
||||
- ./reports:/ci/reports
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -0,0 +1,63 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox: &netbox
|
||||
image: netboxcommunity/netbox:${VERSION-v3.6.1}
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
- netbox-worker
|
||||
env_file: env/netbox.env
|
||||
user: 'unit:root'
|
||||
volumes:
|
||||
- ./configuration:/etc/netbox/config:z,ro
|
||||
- netbox-media-files:/opt/netbox/netbox/media:z
|
||||
netbox-worker:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command:
|
||||
- /opt/netbox/venv/bin/python
|
||||
- /opt/netbox/netbox/manage.py
|
||||
- rqworker
|
||||
netbox-housekeeping:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command:
|
||||
- /opt/netbox/housekeeping.sh
|
||||
|
||||
# postgres
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
env_file: env/postgres.env
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
|
||||
# redis
|
||||
redis:
|
||||
image: redis:7-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
|
||||
env_file: env/redis.env
|
||||
volumes:
|
||||
- netbox-redis-data:/data
|
||||
redis-cache:
|
||||
image: redis:7-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
env_file: env/redis-cache.env
|
||||
|
||||
volumes:
|
||||
netbox-media-files:
|
||||
driver: local
|
||||
netbox-postgres-data:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
DB_HOST=postgres
|
||||
DB_NAME=netbox
|
||||
DB_PASSWORD=J5brHrAXFLQSif0K
|
||||
DB_USER=netbox
|
||||
EMAIL_FROM=netbox@bar.com
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_SERVER=localhost
|
||||
EMAIL_SSL_CERTFILE=
|
||||
EMAIL_SSL_KEYFILE=
|
||||
EMAIL_TIMEOUT=5
|
||||
EMAIL_USERNAME=netbox
|
||||
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
|
||||
EMAIL_USE_SSL=false
|
||||
EMAIL_USE_TLS=false
|
||||
GRAPHQL_ENABLED=true
|
||||
HOUSEKEEPING_INTERVAL=86400
|
||||
MAX_PAGE_SIZE=1000
|
||||
MEDIA_ROOT=/opt/netbox/netbox/media
|
||||
METRICS_ENABLED=false
|
||||
NAPALM_PASSWORD=
|
||||
NAPALM_TIMEOUT=10
|
||||
NAPALM_USERNAME=
|
||||
REDIS_CACHE_DATABASE=1
|
||||
REDIS_CACHE_HOST=redis-cache
|
||||
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
REDIS_CACHE_SSL=false
|
||||
REDIS_DATABASE=0
|
||||
REDIS_HOST=redis
|
||||
REDIS_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
REDIS_SSL=false
|
||||
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
|
||||
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
SKIP_SUPERUSER=false
|
||||
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
|
||||
SUPERUSER_EMAIL=admin@example.com
|
||||
SUPERUSER_NAME=admin
|
||||
SUPERUSER_PASSWORD=admin
|
||||
WEBHOOKS_ENABLED=true
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
POSTGRES_DB=netbox
|
||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
||||
POSTGRES_USER=netbox
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
@@ -0,0 +1,2 @@
|
||||
coverage==7.2.2
|
||||
unittest-xml-reporting==3.2.0
|
||||
@@ -0,0 +1,18 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
__version__ = "1.5"
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = "netbox_slm"
|
||||
verbose_name = "Software Lifecycle Management"
|
||||
description = "Software Lifecycle Management Netbox Plugin."
|
||||
version = __version__
|
||||
author = "ICTU"
|
||||
author_email = "open-source-projects@ictu.nl"
|
||||
base_url = "slm"
|
||||
required_settings = []
|
||||
default_settings = {"version_info": False}
|
||||
|
||||
|
||||
config = SLMConfig
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
if settings.DEBUG:
|
||||
for model in apps.get_app_config("netbox_slm").get_models():
|
||||
admin.site.register(model)
|
||||
@@ -0,0 +1,103 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = [
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"tags",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj.manufacturer} - {obj}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = [
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"software_product",
|
||||
"tags",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = [
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = [
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
@@ -0,0 +1,17 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from netbox_slm.api.views import (
|
||||
NetboxSLMRootView,
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
SoftwareProductInstallationViewSet,
|
||||
SoftwareLicenseViewSet,
|
||||
)
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = NetboxSLMRootView
|
||||
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,49 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox_slm.api.serializers import (
|
||||
SoftwareProductSerializer,
|
||||
SoftwareProductVersionSerializer,
|
||||
SoftwareProductInstallationSerializer,
|
||||
SoftwareLicenseSerializer,
|
||||
)
|
||||
from netbox_slm.filtersets import (
|
||||
SoftwareProductFilterSet,
|
||||
SoftwareProductVersionFilterSet,
|
||||
SoftwareProductInstallationFilterSet,
|
||||
SoftwareLicenseFilterSet,
|
||||
)
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
"""
|
||||
NetboxSLM API root view
|
||||
"""
|
||||
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilterSet
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilterSet
|
||||
|
||||
|
||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilterSet
|
||||
|
||||
|
||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
serializer_class = SoftwareLicenseSerializer
|
||||
filterset_class = SoftwareLicenseFilterSet
|
||||
@@ -0,0 +1,79 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) | Q(manufacturer__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductVersion instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("software_product",)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductInstallation instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("software_product",)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareLicense instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
| Q(installation__device__name__icontains=value)
|
||||
| Q(installation__virtualmachine__name__icontains=value)
|
||||
| Q(installation__cluster__name__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -0,0 +1,24 @@
|
||||
from .software_license import (
|
||||
SoftwareLicenseForm,
|
||||
SoftwareLicenseFilterForm,
|
||||
SoftwareLicenseImportForm,
|
||||
SoftwareLicenseBulkEditForm,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductForm,
|
||||
SoftwareProductFilterForm,
|
||||
SoftwareProductImportForm,
|
||||
SoftwareProductBulkEditForm,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationForm,
|
||||
SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationImportForm,
|
||||
SoftwareProductInstallationBulkEditForm,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionForm,
|
||||
SoftwareProductVersionFilterForm,
|
||||
SoftwareProductVersionImportForm,
|
||||
SoftwareProductVersionBulkEditForm,
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
from django.forms import DateField
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField, LaxURLField
|
||||
from utilities.forms.widgets import APISelect, DatePicker
|
||||
|
||||
|
||||
class SoftwareLicenseForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareLicense object."""
|
||||
|
||||
stored_location_url = LaxURLField(required=False)
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params={
|
||||
"software_product": "$software_product",
|
||||
},
|
||||
)
|
||||
installation = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
||||
),
|
||||
query_params={
|
||||
"software_product": "$software_product",
|
||||
},
|
||||
)
|
||||
start_date = DateField(label=_("Start date"), required=False, widget=DatePicker())
|
||||
expiration_date = DateField(label=_("Expiration date"), required=False, widget=DatePicker())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"q",
|
||||
"tag",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareLicenseImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
|
||||
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
|
||||
installation = DynamicModelChoiceField(queryset=SoftwareProductInstallation.objects.all(), required=False)
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description", "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = ((None, ("q", "tag")),)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"name",
|
||||
"manufacturer",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
nullable_fields = []
|
||||
@@ -0,0 +1,83 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import APISelect
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
||||
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
|
||||
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params={
|
||||
"software_product": "$software_product",
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("device", "virtualmachine", "cluster", "software_product", "version", "tags")
|
||||
|
||||
def clean_version(self):
|
||||
version = self.cleaned_data["version"]
|
||||
software_product = self.cleaned_data["software_product"]
|
||||
if version not in software_product.softwareproductversion_set.all():
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
f"Version `{version}` doesn't exist on {software_product}, make sure you've "
|
||||
f"selected a compatible version or first select the software product."
|
||||
)
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"q",
|
||||
"tag",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductInstallationImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
|
||||
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"software_product",
|
||||
"version",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import APISelect
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product", "tags")
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = ((None, ("q", "tag")),)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductVersionImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
nullable_fields = []
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 3.2.9 on 2022-04-07 12:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_slm', '0002_alter_softwareproduct_manufacturer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.2.9 on 2022-04-15 07:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0153_created_datetimefield'),
|
||||
('virtualization', '0029_created_datetimefield'),
|
||||
('netbox_slm', '0003_auto_20220407_1209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='version',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='virtualmachine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.1.8 on 2023-06-27 08:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('netbox_slm', '0004_auto_20220415_0705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SoftwareLicense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('type', models.CharField(max_length=128)),
|
||||
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expiration_date', models.DateField(blank=True, null=True)),
|
||||
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
|
||||
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-22 00:34
|
||||
|
||||
from django.db import migrations
|
||||
import netbox_slm.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_slm', '0005_add_software_license'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwarelicense',
|
||||
name='stored_location_url',
|
||||
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-04 10:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0036_virtualmachine_config_template'),
|
||||
('netbox_slm', '0006_softwarelicense_stored_location_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwarelicense',
|
||||
name='installation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='softwareproductinstallation',
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,147 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import safestring
|
||||
|
||||
from netbox.models import NetBoxModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import EnhancedURLValidator
|
||||
|
||||
|
||||
class LaxURLField(models.URLField):
|
||||
"""
|
||||
NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField
|
||||
Overriding default_validators is needed, as they are always added
|
||||
"""
|
||||
|
||||
default_validators = [EnhancedURLValidator()]
|
||||
|
||||
|
||||
class SoftwareProduct(NetBoxModel):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
return (
|
||||
safestring.mark_safe(
|
||||
'<a href="{url}">{count}</a>'.format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count,
|
||||
)
|
||||
)
|
||||
if count
|
||||
else "0"
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersion(NetBoxModel):
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
software_product = models.ForeignKey(
|
||||
to="netbox_slm.SoftwareProduct",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
return (
|
||||
safestring.mark_safe(
|
||||
'<a href="{url}">{count}</a>'.format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count,
|
||||
)
|
||||
)
|
||||
if count
|
||||
else "0"
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallation(NetBoxModel):
|
||||
device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
|
||||
virtualmachine = models.ForeignKey(
|
||||
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
|
||||
)
|
||||
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
|
||||
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
|
||||
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk} ({self.platform})"
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name="%(app_label)s_%(class)s_platform",
|
||||
check=(
|
||||
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
|
||||
),
|
||||
violation_error_message="Installation requires exactly one platform destination.",
|
||||
)
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
return self.device or self.virtualmachine or self.cluster
|
||||
|
||||
def render_type(self):
|
||||
if self.device:
|
||||
return "device"
|
||||
if self.virtualmachine:
|
||||
return "virtualmachine"
|
||||
return "cluster"
|
||||
|
||||
|
||||
class SoftwareLicense(NetBoxModel):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
type = models.CharField(max_length=128)
|
||||
|
||||
stored_location = models.CharField(max_length=255, null=True, blank=True)
|
||||
stored_location_url = LaxURLField(max_length=1024, null=True, blank=True)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
expiration_date = models.DateField(null=True, blank=True)
|
||||
|
||||
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
|
||||
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
|
||||
installation = models.ForeignKey(
|
||||
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
||||
|
||||
@property
|
||||
def stored_location_txt(self):
|
||||
if self.stored_location_url and not self.stored_location:
|
||||
return "Link"
|
||||
return self.stored_location
|
||||
@@ -3,8 +3,8 @@ from utilities.choices import ButtonColorChoices
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproduct_list',
|
||||
link_text='Software Products',
|
||||
link="plugins:netbox_slm:softwareproduct_list",
|
||||
link_text="Software Products",
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_add",
|
||||
@@ -20,11 +20,11 @@ menu_items = (
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductversion_list',
|
||||
link_text='Versions',
|
||||
link="plugins:netbox_slm:softwareproductversion_list",
|
||||
link_text="Versions",
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_add",
|
||||
@@ -40,11 +40,11 @@ menu_items = (
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductinstallation_list',
|
||||
link_text='Installations',
|
||||
link="plugins:netbox_slm:softwareproductinstallation_list",
|
||||
link_text="Installations",
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_add",
|
||||
@@ -60,6 +60,26 @@ menu_items = (
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwarelicense_list",
|
||||
link_text="Licenses",
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_add",
|
||||
"Add",
|
||||
"mdi mdi-plus-thick",
|
||||
ButtonColorChoices.GREEN,
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,190 @@
|
||||
import django_tables2 as tables
|
||||
from django.db.models import Count, F, Value
|
||||
|
||||
from netbox.tables import NetBoxTable, ToggleColumn, columns
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"name",
|
||||
"description",
|
||||
"installations",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name="Version")
|
||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"name",
|
||||
"installations",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
device = tables.Column(accessor="device", linkify=True)
|
||||
virtualmachine = tables.Column(accessor="virtualmachine", linkify=True)
|
||||
cluster = tables.Column(accessor="cluster", linkify=True)
|
||||
platform = tables.Column(accessor="platform", linkify=True)
|
||||
type = tables.Column(accessor="render_type")
|
||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_platform(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True
|
||||
|
||||
def order_type(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
|
||||
|
||||
class SoftwareLicenseTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareLicense objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
type = tables.Column()
|
||||
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
|
||||
|
||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
installation = tables.Column(accessor="installation", linkify=True)
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
|
||||
def render_installation(self, **kwargs):
|
||||
return f"{kwargs['record'].installation.platform}"
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software License
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stored location</th>
|
||||
{% if object.stored_location_url %}
|
||||
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
|
||||
{% else %}
|
||||
<td>{{ object.stored_location_txt }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Start date</th>
|
||||
<td>{{ object.start_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Expiration date</th>
|
||||
<td>{{ object.expiration_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object.installation }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ object.manufacturer }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
{% for version in object.softwareproduct_versions.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
||||
</a>
|
||||
{% empty %}
|
||||
n/a
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Installation
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.device %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ object.device }}</td>
|
||||
</tr>
|
||||
{% elif object.virtualmachine %}
|
||||
<tr>
|
||||
<th scope="row">Virtualmachine</th>
|
||||
<td>{{ object.virtualmachine }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">Cluster</th>
|
||||
<td>{{ object.cluster }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Version
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>
|
||||
{{ object.get_installation_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class ModelTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.p_name = "test product"
|
||||
self.v_name = "test version"
|
||||
self.l_name = "test license"
|
||||
|
||||
manufacturer = Manufacturer.objects.create(name="test manufacturer")
|
||||
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
|
||||
device_role = DeviceRole.objects.create(name="test device role")
|
||||
site = Site.objects.create(name="test site")
|
||||
self.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
|
||||
self.vm = VirtualMachine.objects.create(name="test VM")
|
||||
cluster_type = ClusterType.objects.create(name="test cluster type")
|
||||
self.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
|
||||
self.test_url = "https://github.com/ICTU/netbox_slm"
|
||||
|
||||
self.software_product = SoftwareProduct.objects.create(name=self.p_name)
|
||||
self.software_product_version = SoftwareProductVersion.objects.create(
|
||||
name=self.v_name, software_product=self.software_product
|
||||
)
|
||||
self.software_product_installation = SoftwareProductInstallation.objects.create(
|
||||
virtualmachine=self.vm, software_product=self.software_product, version=self.software_product_version
|
||||
)
|
||||
self.software_license = SoftwareLicense.objects.create(
|
||||
name=self.l_name,
|
||||
software_product=self.software_product,
|
||||
version=self.software_product_version,
|
||||
installation=self.software_product_installation,
|
||||
stored_location_url=self.test_url,
|
||||
)
|
||||
|
||||
def test_model_name(self):
|
||||
self.assertEqual(self.p_name, str(self.software_product))
|
||||
self.assertEqual(self.v_name, str(self.software_product_version))
|
||||
self.assertTrue(str(self.software_product_installation)[0].isdigit()) # starts with PK
|
||||
self.assertEqual(self.l_name, str(self.software_license))
|
||||
|
||||
def test_absolute_url(self):
|
||||
self.assertEqual("/plugins/slm/software-products/1/", self.software_product.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/versions/1/", self.software_product_version.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/installations/1/", self.software_product_installation.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url())
|
||||
|
||||
def test_get_installation_count(self):
|
||||
installation_ss = '<a href="/plugins/slm/installations/?q={}">1</a>'
|
||||
self.assertEqual(installation_ss.format(self.p_name), self.software_product.get_installation_count())
|
||||
self.assertEqual(installation_ss.format(self.v_name), self.software_product_version.get_installation_count())
|
||||
|
||||
def test_product_installation_methods(self):
|
||||
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.vm, self.software_product_installation.platform)
|
||||
|
||||
self.software_product_installation.virtualmachine = None
|
||||
self.software_product_installation.device = self.device
|
||||
self.software_product_installation.save()
|
||||
self.assertEqual("device", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.device, self.software_product_installation.platform)
|
||||
|
||||
self.software_product_installation.device = None
|
||||
self.software_product_installation.cluster = self.cluster
|
||||
self.software_product_installation.save()
|
||||
self.assertEqual("cluster", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.cluster, self.software_product_installation.platform)
|
||||
|
||||
def test_software_license_stored_location_txt(self):
|
||||
self.assertEqual("Link", self.software_license.stored_location_txt)
|
||||
|
||||
self.software_license.stored_location = "GitHub"
|
||||
self.software_license.save()
|
||||
self.assertEqual("GitHub", self.software_license.stored_location_txt)
|
||||
@@ -0,0 +1,114 @@
|
||||
from django.urls import path
|
||||
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
urlpatterns = [
|
||||
# SoftwareProduct
|
||||
path("software-products/", views.SoftwareProductListView.as_view(), name="softwareproduct_list"),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name="softwareproduct_add"),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
||||
path(
|
||||
"software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"
|
||||
),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
||||
path(
|
||||
"software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"
|
||||
),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
||||
path(
|
||||
"software-products/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproduct_changelog",
|
||||
kwargs={"model": SoftwareProduct},
|
||||
),
|
||||
# SoftwareProductVersion
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name="softwareproductversion_list"),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_add"),
|
||||
path(
|
||||
"versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"
|
||||
),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
||||
path(
|
||||
"versions/delete/",
|
||||
views.SoftwareProductVersionBulkDeleteView.as_view(),
|
||||
name="softwareproductversion_bulk_delete",
|
||||
),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
||||
path(
|
||||
"versions/<int:pk>/delete/",
|
||||
views.SoftwareProductVersionDeleteView.as_view(),
|
||||
name="softwareproductversion_delete",
|
||||
),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
||||
path(
|
||||
"versions/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductversion_changelog",
|
||||
kwargs={"model": SoftwareProductVersion},
|
||||
),
|
||||
# SoftwareProductInstallation
|
||||
path(
|
||||
"installations/",
|
||||
views.SoftwareProductInstallationListView.as_view(),
|
||||
name="softwareproductinstallation_list",
|
||||
),
|
||||
path(
|
||||
"installations/add/",
|
||||
views.SoftwareProductInstallationEditView.as_view(),
|
||||
name="softwareproductinstallation_add",
|
||||
),
|
||||
path(
|
||||
"installations/import/",
|
||||
views.SoftwareProductInstallationBulkImportView.as_view(),
|
||||
name="softwareproductinstallation_import",
|
||||
),
|
||||
path(
|
||||
"installations/edit/",
|
||||
views.SoftwareProductInstallationBulkEditView.as_view(),
|
||||
name="softwareproductinstallation_bulk_edit",
|
||||
),
|
||||
path(
|
||||
"installations/delete/",
|
||||
views.SoftwareProductInstallationBulkDeleteView.as_view(),
|
||||
name="softwareproductinstallation_bulk_delete",
|
||||
),
|
||||
path(
|
||||
"installations/<int:pk>/",
|
||||
views.SoftwareProductInstallationView.as_view(),
|
||||
name="softwareproductinstallation",
|
||||
),
|
||||
path(
|
||||
"installations/<int:pk>/delete/",
|
||||
views.SoftwareProductInstallationDeleteView.as_view(),
|
||||
name="softwareproductinstallation_delete",
|
||||
),
|
||||
path(
|
||||
"installations/<int:pk>/edit/",
|
||||
views.SoftwareProductInstallationEditView.as_view(),
|
||||
name="softwareproductinstallation_edit",
|
||||
),
|
||||
path(
|
||||
"installations/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductinstallation_changelog",
|
||||
kwargs={"model": SoftwareProductInstallation},
|
||||
),
|
||||
# SoftwareLicense
|
||||
path("licenses/", views.SoftwareLicenseListView.as_view(), name="softwarelicense_list"),
|
||||
path("licenses/add/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_add"),
|
||||
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(), name="softwarelicense_import"),
|
||||
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(), name="softwarelicense_bulk_edit"),
|
||||
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(), name="softwarelicense_bulk_delete"),
|
||||
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(), name="softwarelicense"),
|
||||
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(), name="softwarelicense_delete"),
|
||||
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_edit"),
|
||||
path(
|
||||
"licenses/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwarelicense_changelog",
|
||||
kwargs={"model": SoftwareLicense},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
from .software_license import (
|
||||
SoftwareLicenseListView,
|
||||
SoftwareLicenseView,
|
||||
SoftwareLicenseEditView,
|
||||
SoftwareLicenseDeleteView,
|
||||
SoftwareLicenseBulkImportView,
|
||||
SoftwareLicenseBulkEditView,
|
||||
SoftwareLicenseBulkDeleteView,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductListView,
|
||||
SoftwareProductView,
|
||||
SoftwareProductEditView,
|
||||
SoftwareProductDeleteView,
|
||||
SoftwareProductBulkImportView,
|
||||
SoftwareProductBulkEditView,
|
||||
SoftwareProductBulkDeleteView,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationListView,
|
||||
SoftwareProductInstallationView,
|
||||
SoftwareProductInstallationEditView,
|
||||
SoftwareProductInstallationDeleteView,
|
||||
SoftwareProductInstallationBulkImportView,
|
||||
SoftwareProductInstallationBulkEditView,
|
||||
SoftwareProductInstallationBulkDeleteView,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionListView,
|
||||
SoftwareProductVersionView,
|
||||
SoftwareProductVersionEditView,
|
||||
SoftwareProductVersionDeleteView,
|
||||
SoftwareProductVersionBulkImportView,
|
||||
SoftwareProductVersionBulkEditView,
|
||||
SoftwareProductVersionBulkDeleteView,
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareLicenseListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareLicenses."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
filterset_form = forms.SoftwareLicenseFilterForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
class SoftwareLicenseView(generic.ObjectView):
|
||||
"""Display SoftwareLicense details"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
class SoftwareLicenseEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareLicense instance."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
form = forms.SoftwareLicenseForm
|
||||
|
||||
|
||||
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareLicense instance"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
class SoftwareLicenseBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
model_form = forms.SoftwareLicenseImportForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
form = forms.SoftwareLicenseBulkEditForm
|
||||
|
||||
|
||||
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
table = tables.SoftwareLicenseTable
|
||||
@@ -0,0 +1,54 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproductversion_set.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = forms.SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = forms.SoftwareProductImportForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
form = forms.SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = tables.SoftwareProductTable
|
||||
@@ -0,0 +1,49 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductInstallation
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = forms.SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = forms.SoftwareProductInstallationImportForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
form = forms.SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
@@ -0,0 +1,53 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = forms.SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = forms.SoftwareProductVersionImportForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
form = forms.SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = tables.SoftwareProductVersionTable
|
||||
@@ -0,0 +1,73 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "netbox-slm"
|
||||
dynamic = ["version"]
|
||||
description = "Software Lifecycle Management Netbox Plugin"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = { text = "Apache-2.0" }
|
||||
authors = [
|
||||
{name = "ICTU", email = "open-source-projects@ictu.nl"},
|
||||
]
|
||||
classifiers = [
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
build = [
|
||||
"build == 1.0.3",
|
||||
"setuptools == 68.2.0",
|
||||
"twine == 3.7.1",
|
||||
]
|
||||
tools = [
|
||||
"black == 23.3.0",
|
||||
"ruff == 0.0.275",
|
||||
]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "netbox_slm.__version__"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["netbox_slm"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ["py39"]
|
||||
color = true
|
||||
check = true
|
||||
diff = true
|
||||
fast = true
|
||||
extend-exclude = '''
|
||||
netbox_slm/migrations/.*.py
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py39"
|
||||
format = "github"
|
||||
src = ["netbox_slm"]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"__init__.py" = [
|
||||
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files
|
||||
"F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files
|
||||
]
|
||||
"netbox_slm/migrations/*.py" = [
|
||||
"E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Project metadata
|
||||
sonar.organization=ictu
|
||||
sonar.projectKey=ICTU_netbox_slm
|
||||
sonar.projectName=netbox-slm
|
||||
sonar.projectVersion=1.4
|
||||
|
||||
# Path is relative to the sonar-project.properties file
|
||||
sonar.sources=netbox_slm
|
||||
sonar.python.version=3.9, 3.10, 3.11
|
||||
|
||||
# Unit tests
|
||||
sonar.python.xunit.reportPath=ci/reports/junit.xml
|
||||
sonar.python.coverage.reportPaths=ci/reports/coverage.xml
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
Copyright 2022 ICTU
|
||||
|
||||
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,3 +0,0 @@
|
||||
include ../LICENSE
|
||||
include ../README.rst
|
||||
recursive-include netbox_slm/templates *
|
||||
@@ -1,17 +0,0 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = 'netbox_slm'
|
||||
verbose_name = 'Software Lifecycle Management'
|
||||
description = 'Software Lifecycle Management'
|
||||
version = '0.94'
|
||||
author = 'Hedde van der Heide'
|
||||
author_email = 'hedde.vanderheide@ictu.nl'
|
||||
base_url = 'slm'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'version_info': False
|
||||
}
|
||||
|
||||
config = SLMConfig
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductVersionInline(admin.TabularInline):
|
||||
model = SoftwareProductVersion
|
||||
fields = ('name',)
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(SoftwareProduct)
|
||||
class SoftwareProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
inlines = (SoftwareProductVersionInline,)
|
||||
@@ -1,40 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import PrimaryModelSerializer, OrganizationalModelSerializer
|
||||
# from netbox_slm.api.nested_serializers import (
|
||||
# NestedRecordSerializer,
|
||||
# NestedNameServerSerializer,
|
||||
# )
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductSerializer(PrimaryModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj.manufacturer.name} - {obj.name}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(PrimaryModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj.name
|
||||
@@ -1,13 +0,0 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox_slm.api.views import (
|
||||
NetboxSLMRootView,
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
)
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = NetboxSLMRootView
|
||||
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
urlpatterns = router.urls
|
||||
@@ -1,44 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox_slm.api.serializers import SoftwareProductSerializer, SoftwareProductVersionSerializer
|
||||
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
"""
|
||||
NetboxSLM API root view
|
||||
"""
|
||||
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(CustomFieldModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilter
|
||||
|
||||
# @action(detail=True, methods=["get"])
|
||||
# def records(self, request, pk=None):
|
||||
# records = Record.objects.filter(zone=pk)
|
||||
# serializer = RecordSerializer(records, many=True, context={"request": request})
|
||||
# return Response(serializer.data)
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(CustomFieldModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilter
|
||||
|
||||
# @action(detail=True, methods=["get"])
|
||||
# def records(self, request, pk=None):
|
||||
# records = Record.objects.filter(zone=pk)
|
||||
# serializer = RecordSerializer(records, many=True, context={"request": request})
|
||||
# return Response(serializer.data)
|
||||
|
||||
# for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py
|
||||
@@ -1,50 +0,0 @@
|
||||
from django.forms import BoundField
|
||||
from django.urls import reverse
|
||||
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class CustomDynamicModelMultipleChoiceField(DynamicModelMultipleChoiceField):
|
||||
def get_bound_field(self, form, field_name):
|
||||
bound_field = BoundField(form, self, field_name)
|
||||
|
||||
# Set initial value based on prescribed child fields (if not already set)
|
||||
if not self.initial and self.initial_params:
|
||||
filter_kwargs = {}
|
||||
for kwarg, child_field in self.initial_params.items():
|
||||
value = form.initial.get(child_field.lstrip("$"))
|
||||
if value:
|
||||
filter_kwargs[kwarg] = value
|
||||
if filter_kwargs:
|
||||
self.initial = self.queryset.filter(**filter_kwargs).first()
|
||||
|
||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||
# will be populated on-demand via the APISelect widget.
|
||||
data = bound_field.value()
|
||||
|
||||
if data:
|
||||
field_name = getattr(self, "to_field_name") or "pk"
|
||||
filter = self.filter(field_name=field_name)
|
||||
try:
|
||||
self.queryset = filter.filter(self.queryset, data)
|
||||
except (TypeError, ValueError):
|
||||
# Catch any error caused by invalid initial data passed from the user
|
||||
self.queryset = self.queryset.none()
|
||||
else:
|
||||
self.queryset = self.queryset.none()
|
||||
|
||||
# Set the data URL on the APISelect widget (if not already set)
|
||||
widget = bound_field.field.widget
|
||||
if not widget.attrs.get("data-url"):
|
||||
app_label = self.queryset.model._meta.app_label
|
||||
model_name = self.queryset.model._meta.model_name
|
||||
|
||||
#
|
||||
# Custom url for work with plugin api
|
||||
#
|
||||
data_url = reverse(
|
||||
"plugins-api:{}-api:{}-list".format(app_label, model_name)
|
||||
)
|
||||
widget.attrs["data-url"] = data_url
|
||||
|
||||
return bound_field
|
||||
@@ -1,75 +0,0 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import PrimaryModelFilterSet
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class BaseFilter(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductFilter(BaseFilter):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",) # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilter(BaseFilter):
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains", label="Version"
|
||||
)
|
||||
# software_product_id = django_filters.ModelMultipleChoiceFilter(
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# label='SoftwareProduct (ID)',
|
||||
# )
|
||||
# software_product = django_filters.ModelMultipleChoiceFilter(
|
||||
# field_name='software_product__name',
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# to_field_name='name',
|
||||
# label='SoftwareProduct (name)',
|
||||
# )
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product") # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilter(BaseFilter):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple() # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(software_product__name__icontains=value) | Q(version__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -1,202 +0,0 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.forms import (
|
||||
CustomFieldModelForm,
|
||||
CustomFieldModelCSVForm,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm,
|
||||
CustomFieldModelFilterForm,
|
||||
)
|
||||
from dcim.models import Manufacturer, Device
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description",) # "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProduct instances."""
|
||||
|
||||
model = SoftwareProduct
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product",) # "tags")
|
||||
|
||||
# def clean(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
# return super(SoftwareProductVersionForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProductVersion instances."""
|
||||
|
||||
model = SoftwareProductVersion
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductVersionCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
|
||||
),
|
||||
query_params={
|
||||
'software_product': '$software_product',
|
||||
}
|
||||
)
|
||||
|
||||
# todo need version and device ?
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("device", "virtualmachine", "software_product", "version",) # "tags")
|
||||
|
||||
def clean(self):
|
||||
if not any([self.cleaned_data['device'], self.cleaned_data['virtualmachine']]):
|
||||
raise forms.ValidationError(_("Installation requires atleast one virtualmachine or device destination."))
|
||||
return super(SoftwareProductInstallationForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProductInstallation instances."""
|
||||
|
||||
model = SoftwareProductInstallation
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductInstallationCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -1,104 +0,0 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import safestring
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProduct(PrimaryModel):
|
||||
"""
|
||||
A SoftwareProduct represents ...
|
||||
"""
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='software_products',
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductVersion(PrimaryModel):
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_versions'
|
||||
)
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductInstallation(PrimaryModel):
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_installations',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
virtualmachine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_installations',
|
||||
null = True,
|
||||
blank = True
|
||||
)
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='software_products'
|
||||
)
|
||||
version = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProductVersion',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_versions'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
def get_platform(self):
|
||||
return self.device or self.virtualmachine
|
||||
|
||||
def render_type(self):
|
||||
return f"{'device' if self.device else 'virtualmachine'}"
|
||||
@@ -1,145 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn
|
||||
|
||||
|
||||
class SoftwareProductTable(BaseTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"name",
|
||||
"description",
|
||||
"installations",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(BaseTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Version')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('software_product__manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"name",
|
||||
"installations",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(BaseTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
device = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True
|
||||
)
|
||||
virtualmachine = tables.Column(
|
||||
accessor=Accessor('virtualmachine'),
|
||||
linkify=True
|
||||
)
|
||||
platform = tables.Column(
|
||||
accessor='get_platform',
|
||||
linkify=True
|
||||
)
|
||||
type = tables.Column(accessor='render_type')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
version = tables.Column(
|
||||
accessor=Accessor('version'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
# "tags",
|
||||
)
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
@@ -1,93 +0,0 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load custom_links %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load view_helpers %}
|
||||
|
||||
{% block header %}
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|plugin_viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
{% endblock %}
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ object }}{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
<div class="object-subtitle">
|
||||
<span>Created {{ object.created|annotated_date }}</span>
|
||||
<span class="separator">·</span>
|
||||
<span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block controls %}
|
||||
{# Clone/Edit/Delete Buttons #}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% plugin_buttons object %}
|
||||
|
||||
{# Extra buttons #}
|
||||
{% block extra_controls %}{% endblock %}
|
||||
|
||||
{% if object.clone_fields and request.user|can_add:object %}
|
||||
{% plugin_clone_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_change:object %}
|
||||
{% plugin_edit_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% plugin_delete_button object %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="control-group">
|
||||
{% custom_links object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
||||
</li>
|
||||
{% endblock tab_items %}
|
||||
{% if perms.extras.view_journalentry %}
|
||||
{% with journal_viewname=object|plugin_viewname:'journal' %}
|
||||
{% url journal_viewname pk=object.pk as journal_url %}
|
||||
{% if journal_url %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
|
||||
Journal {% badge object.journal_entries.count %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if perms.extras.view_objectchange %}
|
||||
{% with changelog_viewname=object|plugin_viewname:'changelog' %}
|
||||
{% url changelog_viewname pk=object.pk as changelog_url %}
|
||||
{% if changelog_url %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,141 +0,0 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load view_helpers %}
|
||||
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
{% if permissions.add and 'add' in action_buttons %}
|
||||
{% add_button content_type.model_class|plugin_viewname:"add" %}
|
||||
{% endif %}
|
||||
{% if permissions.add and 'import' in action_buttons %}
|
||||
{% import_button content_type.model_class|plugin_viewname:"import" %}
|
||||
{% endif %}
|
||||
{% if 'export' in action_buttons %}
|
||||
{% export_button content_type %}
|
||||
{% endif %}
|
||||
</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="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
|
||||
{% badge table.page.paginator.count %}
|
||||
</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 %}
|
||||
<div class="tab-content">
|
||||
|
||||
{# Object list #}
|
||||
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
|
||||
{# Applied filters #}
|
||||
{% if filter_form %}
|
||||
{% applied_filters filter_form request.GET %}
|
||||
{% endif %}
|
||||
|
||||
{# "Select all" form #}
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
|
||||
<div id="select-all-box" class="d-none card noprint">
|
||||
<form method="post" class="form col-md-12">
|
||||
{% csrf_token %}
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
{% if bulk_edit_url and permissions.change %}
|
||||
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bulk_delete_url and permissions.delete %}
|
||||
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{# Object table controls #}
|
||||
{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
|
||||
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
|
||||
{# Object table #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form buttons #}
|
||||
{% if permissions.change or permissions.delete %}
|
||||
{% with bulk_edit_url=content_type.model_class|plugin_validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|plugin_validated_viewname:"bulk_delete" %}
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% block bulk_buttons %}{% endblock %}
|
||||
{% if bulk_edit_url and permissions.change %}
|
||||
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bulk_delete_url and permissions.delete %}
|
||||
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
{# Paginator #}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
|
||||
{# Filter form #}
|
||||
{% 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>
|
||||
|
||||
{# Table config form #}
|
||||
{% table_config_form table table_name="ObjectTable" %}
|
||||
{% endblock content-wrapper %}
|
||||
@@ -1,76 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
{% for version in object.softwareproduct_versions.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Versions</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">NAME</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in versions %}
|
||||
<tr>
|
||||
<td>{{ version.name|truncatechars:32 }}</td>
|
||||
<td class="noprint text-end text-nowrap">
|
||||
{% if request.user|can_change:object %}
|
||||
{% plugin_edit_button version %}
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% plugin_delete_button version %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,82 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">bla</th>
|
||||
<td>
|
||||
<!-- {% for version in object.softwareproduct_versions.all %}-->
|
||||
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
|
||||
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
|
||||
<!-- </a>-->
|
||||
<!-- {% endfor %}-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,78 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>
|
||||
{{ object.get_installation_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,91 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% comment %}
|
||||
<a href="{% url 'plugins:netbox_slm:record_add' %}?zone={{ object.id }}" class="btn btn-sm btn-primary" role="button">
|
||||
<i class="mdi mdi-plus-thick"></i> Add Record
|
||||
</a>
|
||||
{% endcomment %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Zone
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
<ul>
|
||||
<!-- {% for nameserver in object.nameservers.all %}-->
|
||||
<!-- <li>-->
|
||||
<!-- <a href="{% url 'plugins:netbox_slm:nameserver' pk=nameserver.pk %}">-->
|
||||
<!-- {{ nameserver }}-->
|
||||
<!-- </a>-->
|
||||
<!-- </li>-->
|
||||
<!-- {% endfor %}-->
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,94 +0,0 @@
|
||||
from django import template
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django import template
|
||||
|
||||
from utilities.templatetags.buttons import _get_viewname
|
||||
from utilities.utils import prepare_cloned_fields
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
def plugin_validated_viewname(model, action):
|
||||
"""Return the view name for :
|
||||
* the given model and action if valid
|
||||
* or the given model and action if valid inside a plugin
|
||||
* or None if invalid.
|
||||
"""
|
||||
core_viewname = f"{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
plugin_viewname = (
|
||||
f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
)
|
||||
try:
|
||||
# Validate and return the view name. We don't return the actual URL yet because many of the templates
|
||||
# are written to pass a name to {% url %}.
|
||||
reverse(core_viewname)
|
||||
return core_viewname
|
||||
except NoReverseMatch:
|
||||
try:
|
||||
reverse(plugin_viewname)
|
||||
return plugin_viewname
|
||||
except NoReverseMatch:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter()
|
||||
def plugin_viewname(model, action):
|
||||
"""
|
||||
Return the view name for the given model and action. Does not perform any validation.
|
||||
"""
|
||||
return f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
|
||||
|
||||
#
|
||||
# Instance buttons
|
||||
#
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/clone.html")
|
||||
def plugin_clone_button(instance):
|
||||
viewname = _get_viewname(instance, "add")
|
||||
url = reverse(f"plugins:{viewname}")
|
||||
|
||||
# Populate cloned field values
|
||||
param_string = prepare_cloned_fields(instance)
|
||||
if param_string:
|
||||
url = f"{url}?{param_string}"
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/edit.html")
|
||||
def plugin_edit_button(instance, **kwargs):
|
||||
viewname = _get_viewname(instance, "edit")
|
||||
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/delete.html")
|
||||
def plugin_delete_button(instance, **kwargs):
|
||||
viewname = _get_viewname(instance, "delete")
|
||||
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# List buttons
|
||||
#
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/add.html")
|
||||
def plugin_add_button(url):
|
||||
url = reverse(f"plugins:{url}")
|
||||
|
||||
return {
|
||||
"add_url": url,
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
from django.urls import path
|
||||
from extras.views import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
|
||||
urlpatterns = [
|
||||
# Software Products
|
||||
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
||||
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
||||
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
||||
path(
|
||||
"software-products/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproduct_changelog",
|
||||
kwargs={"model": SoftwareProduct},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
|
||||
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
||||
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), name="softwareproductversion_bulk_delete"),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
||||
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
||||
path(
|
||||
"versions/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductversion_changelog",
|
||||
kwargs={"model": SoftwareProductVersion},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("installations/", views.SoftwareProductInstallationListView.as_view(), name='softwareproductinstallation_list'),
|
||||
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), name='softwareproductinstallation_add'),
|
||||
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), name="softwareproductinstallation_import"),
|
||||
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), name="softwareproductinstallation_bulk_edit"),
|
||||
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_bulk_delete"),
|
||||
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(), name="softwareproductinstallation"),
|
||||
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(), name="softwareproductinstallation_delete"),
|
||||
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(), name="softwareproductinstallation_edit"),
|
||||
path(
|
||||
"installations/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductinstallation_changelog",
|
||||
kwargs={"model": SoftwareProductInstallation},
|
||||
),
|
||||
]
|
||||
@@ -1,165 +0,0 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter
|
||||
from netbox_slm.forms import (
|
||||
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm,
|
||||
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
|
||||
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
)
|
||||
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
filterset_form = SoftwareProductFilterForm
|
||||
table = SoftwareProductTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproduct_versions.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = SoftwareProductCSVForm
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
table = SoftwareProductTable
|
||||
form = SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
filterset_form = SoftwareProductVersionFilterForm
|
||||
table = SoftwareProductVersionTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = SoftwareProductVersionCSVForm
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
table = SoftwareProductVersionTable
|
||||
form = SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
filterset_form = SoftwareProductInstallationFilterForm
|
||||
table = SoftwareProductInstallationTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
# def get_extra_context(self, request, instance):
|
||||
# records = instance.record_set.all()
|
||||
# return {"records": records}
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = SoftwareProductInstallationCSVForm
|
||||
table = SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
table = SoftwareProductInstallationTable
|
||||
form = SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = SoftwareProductInstallationTable
|
||||
@@ -1,28 +0,0 @@
|
||||
[metadata]
|
||||
name = netbox-slm
|
||||
version = 0.94
|
||||
description = Software Lifecycle Management Netbox Plugin.
|
||||
# long_description = file: README.rst
|
||||
# long_description_content_type='text/x-rst'
|
||||
author = Hedde van der Heide
|
||||
author_email = hedde.vanderheide@ictu.nl
|
||||
license = Apache 2.0
|
||||
classifiers =
|
||||
Environment :: Web Environment
|
||||
Framework :: Django
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Topic :: Internet :: WWW/HTTP
|
||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
|
||||
[options]
|
||||
include_package_data = true
|
||||
packages = find:
|
||||
python_requires = >=3.7
|
||||
@@ -1,3 +0,0 @@
|
||||
from setuptools import setup, Extension
|
||||
|
||||
setup()
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
git clone -b release https://github.com/netbox-community/netbox-docker.git
|
||||
cd netbox-docker
|
||||
tee docker-compose.override.yml <<EOF
|
||||
version: '3.4'
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
|
||||
Reference in New Issue
Block a user