26 Commits
Author SHA1 Message Date
wkoot 1411e3a5cb Update MANIFEST.in, so all files are included (#34)
Closes #32
2023-12-08 10:14:23 +01:00
wkoot d082af4a9b Add Installation foreign key to VM Cluster (#29)
Closes #17
2023-10-04 18:23:08 +02:00
wkoot 2501e41b7c Add 'stored_location_url' to SoftwareLicense (#28)
Closes #27
2023-09-22 02:42:02 +02:00
wkootandManuel Hartung aa41f8a402 Assorted fixes (#25)
* Updated imports for Netbox 3.6.1

Closes #21 and #23

* Changed Fieldtypes of Start and Expiration Date

Closes #22

* Bump to NetBox v3.6.1

* Add pyproject.toml and python workflow

---------

Co-authored-by: Manuel Hartung <manuel.hartung@atos.net>
2023-09-15 09:41:36 +02:00
wkoot 6c512e0e9b Add SoftwareLicense (#20) 2023-06-30 15:11:28 +02:00
wkoot 5b96496b46 Replace NetBoxModelCSVForm with NetBoxModelImportForm (#19)
Closes #18
2023-06-06 21:16:13 +02:00
wkoot 88edcde8d6 Unify api serializers (#16) 2023-04-19 10:14:32 +02:00
wkoot 7f18a3ad6f Version bump NetBox 3.4.7 (#15)
Closes #14
2023-03-31 10:02:35 +02:00
wkoot 1925be271a Set up continuous integration (#13)
Closes #3
2023-03-29 22:00:48 +02:00
Hedde van der Heide a53486ca23 bump version to 1.2 2022-05-19 11:08:57 +02:00
Hedde van der Heide 4bc418a5d0 fixed filter bug versions api and set required on manufacturer for softwareproducts add 2022-05-19 11:06:34 +02:00
Hedde van der Heide e952111a53 version bump 1.1 2022-05-03 15:21:18 +02:00
Hedde van der Heide e73cb5fa1a Merge branch 'master' of https://github.com/ICTU/netbox_slm 2022-05-03 15:19:55 +02:00
Hedde van der Heide 56552bdd02 fixed filter search 2022-05-03 15:19:53 +02:00
Hedde van der Heide f914e78812 Update README.md 2022-05-02 12:39:47 +02:00
Hedde van der Heide c9a7b2315e Update README.md 2022-05-02 12:37:36 +02:00
Hedde van der Heide a8c9fc5fbe Delete LICENSE.md 2022-05-02 12:37:18 +02:00
Hedde van der Heide a1f6e3dc92 Create LICENSE 2022-05-02 12:37:11 +02:00
Hedde van der Heide 4f8053c0f6 Rename LICENSE to LICENSE.md 2022-05-02 12:32:20 +02:00
Hedde van der Heide 3b3a6d63d4 Update and rename README.rst to README.md 2022-05-02 12:32:08 +02:00
Hedde van der Heide ce283dea52 updated README 2022-04-22 14:04:29 +02:00
Hedde van der Heide f3579cce63 updated README 2022-04-22 13:57:49 +02:00
Hedde van der Heide 3bb711ea84 updated README 2022-04-22 13:57:45 +02:00
Hedde van der Heide 9d76bb421d normalized project structure 2022-04-22 13:52:26 +02:00
Hedde van der Heide 7a24221444 moved top level files 2022-04-22 13:33:17 +02:00
Hedde van der Heide ac198ef7cf Large overhaul of naming conventions and code clean ups 2022-04-22 13:30:10 +02:00
77 changed files with 2220 additions and 1485 deletions
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
COMPOSE_PROJECT_NAME=netbox-docker
+36
View File
@@ -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 }}
+28
View File
@@ -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
+6
View File
@@ -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/
+2 -2
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
include LICENSE
include README.md
include SECURITY.md
graft netbox_slm
prune netbox_slm/tests
global-exclude *.py[cod]
global-exclude __pycache__
-12
View File
@@ -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
View File
@@ -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"
}
}
}
+64 -57
View File
@@ -1,43 +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
~~~~~~~~~~~~~~~~~~
WARNING: This plugin is only tested with Netbox 3.2 at this time.
## Known Issues
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``.
- 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)
Next, follow these instructions (based on the `Netbox docker variant
instructions <https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files>`__)
## 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/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.98`` 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.2.X
```
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:
@@ -50,102 +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
$ 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.
Steps
~~~~~
::
### Steps
from your projects directory clone the netbox repository
```
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
```
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 theres 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
@@ -153,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:
@@ -201,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.
-215
View File
@@ -1,215 +0,0 @@
Software Lifecycle Management
-----------------------------
*Netbox Plugin for lifecycle management of software components, including versions and installations*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------
Installation Guide
~~~~~~~~~~~~~~~~~~
WARNING: This plugin is only tested with Netbox 3.2 at this time.
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/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.98`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
.. code:: dockerfile
FROM netboxcommunity/netbox:v3.2.X
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.
Steps
~~~~~
::
from your projects directory clone the netbox repository
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
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 theres 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
View File
@@ -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.
+17
View File
@@ -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
+7
View File
@@ -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
+257
View File
@@ -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')
+11
View File
@@ -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',
)
}
+23
View File
@@ -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"
+63
View File
@@ -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
+42
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_DB=netbox
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
POSTGRES_USER=netbox
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=H733Kdjndks81
View File
+2
View File
@@ -0,0 +1,2 @@
coverage==7.2.2
unittest-xml-reporting==3.2.0
+18
View File
@@ -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
+7
View File
@@ -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)
+103
View File
@@ -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}"
@@ -4,6 +4,7 @@ from netbox_slm.api.views import (
SoftwareProductViewSet,
SoftwareProductVersionViewSet,
SoftwareProductInstallationViewSet,
SoftwareLicenseViewSet,
)
router = NetBoxRouter()
@@ -12,4 +13,5 @@ router.APIRootView = NetboxSLMRootView
router.register("softwareproducts", SoftwareProductViewSet)
router.register("softwareproductversions", SoftwareProductVersionViewSet)
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
urlpatterns = router.urls
router.register("softwarelicenses", SoftwareLicenseViewSet)
urlpatterns = router.urls
+49
View File
@@ -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
+79
View File
@@ -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)
+24
View File
@@ -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,
)
+102
View File
@@ -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",
),
),
)
+46
View File
@@ -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,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.'),
),
]
+147
View File
@@ -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"],
),
),
),
)
+190
View File
@@ -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 %}
@@ -15,7 +15,15 @@
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object }}</td>
<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>
@@ -34,7 +42,6 @@
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
@@ -13,17 +13,22 @@
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% if object.device %}
{% if object.device %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device }}</td>
</tr>
{% else %}
{% elif object.virtualmachine %}
<tr>
<th scope="row">Virtualmachine</th>
<td>{{ object.virtualmachine }}</td>
</tr>
{% endif %}
{% 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>
@@ -37,7 +42,6 @@
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
@@ -28,7 +28,6 @@
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
View File
+77
View File
@@ -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)
+114
View File
@@ -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},
),
]
+36
View File
@@ -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,
)
+49
View File
@@ -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
+54
View File
@@ -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
+73
View File
@@ -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
]
+13
View File
@@ -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
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
include ../LICENSE
include ../README.rst
recursive-include netbox_slm/templates *
-17
View File
@@ -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.98'
author = 'Hedde van der Heide'
author_email = 'hedde.vanderheide@ictu.nl'
base_url = 'slm'
required_settings = []
default_settings = {
'version_info': False
}
config = SLMConfig
-14
View File
@@ -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,)
-55
View File
@@ -1,55 +0,0 @@
from rest_framework import serializers
from netbox.api.serializers import NetBoxModelSerializer
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
)
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.name} - {obj.name}"
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 obj.name
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', 'software_product', 'version', 'tags',
'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj
-42
View File
@@ -1,42 +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 netbox.api.viewsets import NetBoxModelViewSet
from netbox_slm.api.serializers import (
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
)
from netbox_slm.filters import (
SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter,
)
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
)
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 = SoftwareProductFilter
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
queryset = SoftwareProductVersion.objects.all()
serializer_class = SoftwareProductVersionSerializer
filterset_class = SoftwareProductVersionFilter
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
queryset = SoftwareProductInstallation.objects.all()
serializer_class = SoftwareProductInstallationSerializer
filterset_class = SoftwareProductInstallationFilter
-50
View File
@@ -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
-75
View File
@@ -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 NetBoxModelFilterSet
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import DynamicModelMultipleChoiceField
class BaseFilter(NetBoxModelFilterSet):
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)
-192
View File
@@ -1,192 +0,0 @@
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from netbox.forms import (
NetBoxModelForm,
NetBoxModelCSVForm,
NetBoxModelBulkEditForm,
NetBoxModelFilterSetForm,
)
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(NetBoxModelForm):
"""Form for creating a new SoftwareProduct object."""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer", "description", "tags")
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
"""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(NetBoxModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name",)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
model = SoftwareProduct
nullable_fields = []
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")
# def clean(self):
# import pdb;pdb.set_trace()
# return super(SoftwareProductVersionForm, self).clean()
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
"""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(NetBoxModelCSVForm):
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 = []
class SoftwareProductInstallationForm(NetBoxModelForm):
"""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',
}
)
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(NetBoxModelFilterSetForm):
"""Form for filtering SoftwareProductInstallation instances."""
model = SoftwareProductInstallation
q = forms.CharField(required=False, label="Search")
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
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',)),
)
# nullable_fields = ('',)
-98
View File
@@ -1,98 +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 # todo this changed in netbox 3.1.X and higher, currently not used but checkout why
from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProduct(NetBoxModel):
"""
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,
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(NetBoxModel):
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT,
)
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(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
)
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}"
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'}"
-167
View File
@@ -1,167 +0,0 @@
import django_tables2 as tables
from django.db.models import Count
from django_tables2.utils import Accessor
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from netbox.tables import NetBoxTable, ChoiceFieldColumn, ToggleColumn, columns
class SoftwareProductTable(NetBoxTable):
"""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 = 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=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 = 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=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 = 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):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
def order_type(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
def render_software_product(self, value, **kwargs):
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
-67
View File
@@ -1,67 +0,0 @@
from django.urls import path
from netbox.views.generic 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},
),
]
-165
View File
@@ -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.softwareproductversion_set.all()
return {"versions": versions}
class SoftwareProductEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProduct instance."""
queryset = SoftwareProduct.objects.all()
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()
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()
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
-28
View File
@@ -1,28 +0,0 @@
[metadata]
name = netbox-slm
version = 0.98
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
-3
View File
@@ -1,3 +0,0 @@
from setuptools import setup, Extension
setup()
+1 -1
View File
@@ -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: