Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f84a6bde2a | ||
|
|
811490e429 | ||
|
|
7ce718ff9d | ||
|
|
9e87afcde7 | ||
|
|
e03195c9b1 | ||
|
|
59730a5184 | ||
|
|
69594bc33a | ||
|
|
766ecfc25e | ||
|
|
d773a407a2 | ||
|
|
175a01b257 | ||
|
|
334a76e640 | ||
|
|
ef45789b30 | ||
|
|
4bd02ab6f9 | ||
|
|
410b8f6617 | ||
|
|
306d853a16 | ||
|
|
193f04a208 | ||
|
|
c3ed952b5f | ||
|
|
87fdeb1c24 | ||
|
|
a0a98bb407 | ||
|
|
3539539c4e |
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
@@ -7,11 +8,16 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build the Docker image
|
||||
run: docker compose build --no-cache
|
||||
@@ -23,14 +29,15 @@ jobs:
|
||||
- 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 && \
|
||||
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"
|
||||
sed -i "s/<source>\/opt\/netbox\/netbox<\/source>/<source><\/source>/" ci/reports/coverage.xml
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Sonarcloud scan
|
||||
- name: Sonar scan
|
||||
if: env.SONAR_TOKEN != null
|
||||
uses: sonarsource/sonarcloud-github-action@master
|
||||
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Markdown lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
vale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: errata-ai/vale-action@85f9f7f2c5f449ac0ae5b66662961bae3f77ca6a # v2.1.2
|
||||
with:
|
||||
fail_on_error: true
|
||||
filter_mode: nofilter
|
||||
vale_flags: "--glob=*.md --config=ci/.vale.ini"
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Python
|
||||
|
||||
on:
|
||||
@@ -7,22 +8,33 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- python-version: 3.11
|
||||
- python-version: 3.12
|
||||
- python-version: 3.13
|
||||
- python-version: 3.14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install tools
|
||||
run: pip install .[tools]
|
||||
- name: Install ci tools
|
||||
run: pip install .[ci]
|
||||
|
||||
- name: Run ruff lint
|
||||
run: ruff check netbox_slm
|
||||
|
||||
- name: Run ruff format
|
||||
run: ruff format netbox_slm
|
||||
run: ruff format --check netbox_slm
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: GitHub Actions Security Analysis with zizmor
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25
|
||||
|
||||
### Changed
|
||||
|
||||
* Support NetBox v4.6.3 (#76)
|
||||
* Support Python 3.14 (#76)
|
||||
|
||||
## [1.8.3](https://github.com/ICTU/netbox_slm/releases/tag/1.8.3) - 2025-12-18
|
||||
|
||||
### Fixed
|
||||
|
||||
* Filters with default order by ID (#75)
|
||||
|
||||
## [1.8.2](https://github.com/ICTU/netbox_slm/releases/tag/1.8.2) - 2025-06-02
|
||||
|
||||
### Fixed
|
||||
|
||||
* Permission check for adding software product installations (#70)
|
||||
|
||||
## [1.8.1](https://github.com/ICTU/netbox_slm/releases/tag/1.8.1) - 2025-04-11
|
||||
|
||||
### Fixed
|
||||
|
||||
* Filtering on foreign objects (#65)
|
||||
|
||||
## [1.8.0](https://github.com/ICTU/netbox_slm/releases/tag/1.8.0) - 2025-02-25
|
||||
|
||||
### Added
|
||||
|
||||
* Toggle to show plugin as top level menu item (#42)
|
||||
* Bulk import and bulk edit (#4)
|
||||
* Changelog with backdated changes (#51)
|
||||
* List filtering (#54)
|
||||
* Show installations on native objects (#49)
|
||||
* SPDX license expressions (#47)
|
||||
|
||||
### Changed
|
||||
|
||||
* Support NetBox v4.2.3 (#51)
|
||||
* Render objects consistently (#58)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Model detail view listing of linked objects (#55)
|
||||
* Search link of installations table cells (#48)
|
||||
|
||||
## [1.7.0](https://github.com/ICTU/netbox_slm/releases/tag/1.7.0) - 2024-07-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Support NetBox v4.0.6 (#38)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Hidden plugin data when not logged in (#37)
|
||||
|
||||
### Removed
|
||||
|
||||
* Broken bulk import and bulk edit (#4)
|
||||
|
||||
## [1.6.0](https://github.com/ICTU/netbox_slm/releases/tag/1.6.0) - 2024-02-13
|
||||
|
||||
### Added
|
||||
|
||||
* Additional `SoftwareProductVersion` fields (#10)
|
||||
* Additional `SoftwareLicense` fields (#26)
|
||||
|
||||
### Changed
|
||||
|
||||
* Support NetBox v3.7.2 (#35)
|
||||
|
||||
## [1.5](https://github.com/ICTU/netbox_slm/releases/tag/1.5) - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
* Hyperlink option for `SoftwareLicense.stored_location` (#27)
|
||||
|
||||
### Changed
|
||||
|
||||
* Update docs and imports for newer NetBox versions (#23)
|
||||
* Support NetBox v3.6.1 (#21)
|
||||
* Allow linking `SoftwareProductInstallation` to `Cluster` (#17)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Date picker for start and expiration dates (#22)
|
||||
|
||||
## [1.4](https://github.com/ICTU/netbox_slm/releases/tag/1.4) - 2023-06-30
|
||||
|
||||
### Added
|
||||
|
||||
* License information for software installations (#8)
|
||||
|
||||
### Changed
|
||||
|
||||
* Replace `NetBoxModelCSVForm` with `NetBoxModelImportForm` (#18)
|
||||
|
||||
## [1.3](https://github.com/ICTU/netbox_slm/releases/tag/1.3) - 2023-04-19
|
||||
|
||||
### Added
|
||||
|
||||
* Set up continuous integration (#3)
|
||||
|
||||
### Changed
|
||||
|
||||
* Support NetBox v3.4.7 (#14)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Unify API serialization (#12)
|
||||
|
||||
## [1.2](https://github.com/ICTU/netbox_slm/releases/tag/1.2) - 2022-05-19
|
||||
|
||||
* Fixed filtering of relevant versions when adding a new installation
|
||||
* Manufacturer is now required for the add software product form
|
||||
|
||||
## [1.1](https://github.com/ICTU/netbox_slm/releases/tag/1.1) - 2022-05-03
|
||||
|
||||
* Fixed search filter
|
||||
@@ -1,13 +1,201 @@
|
||||
Copyright 2022 ICTU
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
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
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
1. Definitions.
|
||||
|
||||
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.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Maintenance related tasks
|
||||
|
||||
|
||||
## Version upgrade workflow
|
||||
|
||||
1. Update version spec in `netbox_slm/__init__.py` and `sonar-project.properties`
|
||||
1. Update NetBox base image versions in `ci/Dockerfile`, `ci/Dockerfile-CI` and `ci/docker-compose.yml`
|
||||
1. Ensure that the `PYTHON_VERSION` in the `Dockerfile`s is the same as the NetBox community docker base image
|
||||
1. Check for any runtime errors and warnings in the `netbox-*` container logs
|
||||
1. Create new version tag on GitHub, following semantic versioning as: `MAJOR.MINOR.PATCH`
|
||||
1. Update the `CHANGELOG.md` with new version information and move `[Unreleased]` items to new version section
|
||||
1. Build the package: `python -m build`
|
||||
1. Upload the distributions to PyPI: `twine upload --skip-existing dist/*`
|
||||
|
||||
|
||||
## 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 PostgreSQL and Redis container instances, basically ignoring the
|
||||
NetBox docker runtime server.
|
||||
|
||||
### Steps
|
||||
|
||||
from your projects directory clone the NetBox repository
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
```
|
||||
|
||||
install the virtual environment
|
||||
|
||||
```shell
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
```
|
||||
|
||||
create and edit `netbox/configuration.py` (based on the template file) add these lines at the end of the file;
|
||||
|
||||
```python
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
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
|
||||
|
||||
```shell
|
||||
$ 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 PostgreSQL ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the Dockerfile(s) accordingly)
|
||||
|
||||
Note, you can also start and stop NetBox by hand:
|
||||
|
||||
```shell
|
||||
$ cd netbox-docker
|
||||
$ docker compose up -d
|
||||
```
|
||||
|
||||
or stop the stack with
|
||||
|
||||
```shell
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
#### to start fresh:
|
||||
|
||||
```shell
|
||||
$ 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 PostgreSQL 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):
|
||||
|
||||
```python
|
||||
# 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,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run commands from the NetBox repository like this;
|
||||
|
||||
```shell
|
||||
$ cd netbox/netbox
|
||||
$ 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 browser to see the auto reloading version of the NetBox UI.
|
||||
Port 8000 is taken by the Docker ran variant.
|
||||
@@ -13,18 +13,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
- WARNING: This plugin is only tested with a single NetBox version at this time.
|
||||
- CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
|
||||
|
||||
## Installation Guide
|
||||
|
||||
When using the Docker version of NetBox, first follow the netbox-docker [quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the netbox-docker repo and set up the ``docker-compose.override.yml``.
|
||||
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` repository 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:
|
||||
Note that this plugin is only tested against a single NetBox version at this time, see [Dockerfile-CI](https://github.com/ICTU/netbox_slm/blob/master/ci/Dockerfile-CI).
|
||||
|
||||
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/plugins.py``.
|
||||
@@ -32,195 +27,35 @@ to install the NetBox SLM plugin:
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
```
|
||||
FROM netboxcommunity/netbox:vX.Y.Z
|
||||
```dockerfile
|
||||
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
|
||||
```
|
||||
COPY ../pyproject.toml /tmp/
|
||||
RUN uv pip install -r /tmp/pyproject.toml
|
||||
```
|
||||
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
```
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- 8000:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-SLM
|
||||
image: netbox:slm
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
```
|
||||
```yaml
|
||||
version: "3.7"
|
||||
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 `netbox_slm/__init__.py` file to reflect the new version, then from the *src*
|
||||
directory run
|
||||
|
||||
```
|
||||
$ python -m build
|
||||
$ twine upload dist/*
|
||||
```
|
||||
|
||||
On Github.com create a similar tag and version. These steps could be
|
||||
automated with a github workflow.
|
||||
|
||||
|
||||
## 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 there’s no processes
|
||||
using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
|
||||
```
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
or stop the stack with
|
||||
|
||||
```
|
||||
$ docker-compose down
|
||||
```
|
||||
|
||||
# to start fresh:
|
||||
|
||||
```
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
```
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and
|
||||
redis connection strings (username, password) to the ones the netbox
|
||||
docker backend is using, for example (using default user and passwords
|
||||
from the netbox docker example):
|
||||
|
||||
```
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
```
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
```
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox_slm/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 browser to see the auto reloading
|
||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
|
||||
|
||||
## Get in touch
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
StylesPath = styles
|
||||
|
||||
MinAlertLevel = suggestion
|
||||
Vocab = Base
|
||||
|
||||
Packages = proselint
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale, proselint
|
||||
|
||||
[ci/styles/**]
|
||||
BasedOnStyles =
|
||||
+9
-8
@@ -1,17 +1,18 @@
|
||||
ARG PYTHON_VERSION=3.11
|
||||
ARG PYTHON_VERSION=3.14
|
||||
FROM python:${PYTHON_VERSION}-alpine AS compile-image
|
||||
# Python version build arg is not actually supported by NetBox container image, only used for maintenance convenience
|
||||
|
||||
WORKDIR /build
|
||||
RUN python -m venv /opt/netbox/venv
|
||||
ENV PATH="/opt/netbox/venv/bin:$PATH"
|
||||
RUN python3 -m venv /opt/netbox/venv &&\
|
||||
source /opt/netbox/venv/bin/activate
|
||||
|
||||
COPY ../ /build
|
||||
|
||||
RUN pip install -U .[build]
|
||||
RUN python -m build
|
||||
RUN pip install --no-index /build
|
||||
RUN pip install -U .[build] &&\
|
||||
python -m build &&\
|
||||
pip install --no-index /build
|
||||
|
||||
FROM netboxcommunity/netbox:v4.0.6
|
||||
FROM netboxcommunity/netbox:v4.6.3
|
||||
ARG PYTHON_VERSION=3.14
|
||||
|
||||
ARG PYTHON_VERSION=3.11
|
||||
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
|
||||
|
||||
+4
-6
@@ -1,7 +1,5 @@
|
||||
FROM netboxcommunity/netbox:v4.0.6
|
||||
FROM netboxcommunity/netbox:v4.6.3
|
||||
ARG PYTHON_VERSION=3.14
|
||||
|
||||
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
|
||||
COPY ../pyproject.toml /tmp/
|
||||
RUN uv pip install -r /tmp/pyproject.toml --extra ci
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Minimal NetBox config for NetBox SLM plugin testing, docs: https://netbox.readthedocs.io/en/stable/configuration/
|
||||
Based on https://github.com/netbox-community/netbox/blob/v4.6.3/netbox/netbox/configuration_testing.py
|
||||
"""
|
||||
from os import environ
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'NAME': environ.get('DB_NAME', 'netbox'),
|
||||
'USER': environ.get('DB_USER', ''),
|
||||
'PASSWORD': environ.get('DB_PASSWORD', ''),
|
||||
'HOST': environ.get('DB_HOST', 'localhost'),
|
||||
'PORT': environ.get('DB_PORT', ''),
|
||||
'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')),
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
|
||||
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
|
||||
|
||||
PLUGINS = ["netbox_slm"]
|
||||
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': environ.get('REDIS_HOST', 'localhost'),
|
||||
'PORT': int(environ.get('REDIS_PORT', 6379)),
|
||||
'PASSWORD': environ.get('REDIS_PASSWORD', ''),
|
||||
'DATABASE': int(environ.get('REDIS_DATABASE', 0)),
|
||||
'SSL': environ.get('REDIS_SSL', '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': 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',
|
||||
},
|
||||
}
|
||||
|
||||
SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
|
||||
|
||||
DEFAULT_PERMISSIONS = {}
|
||||
|
||||
API_TOKEN_PEPPERS = {
|
||||
1: 'TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE',
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
####
|
||||
## 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')
|
||||
@@ -1,11 +0,0 @@
|
||||
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',
|
||||
)
|
||||
}
|
||||
@@ -8,13 +8,22 @@ services:
|
||||
dockerfile: ci/Dockerfile-CI
|
||||
image: netbox:slm
|
||||
environment:
|
||||
- COVERAGE_FILE=/ci/.coverage
|
||||
- COVERAGE_FILE=/tmp/.coverage
|
||||
healthcheck:
|
||||
disable: true
|
||||
volumes:
|
||||
- ./reports:/ci/reports
|
||||
- ./config:/etc/netbox/config:z,ro
|
||||
- ./reports:/ci/reports:rw
|
||||
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
depends_on:
|
||||
- netbox
|
||||
healthcheck:
|
||||
disable: true
|
||||
volumes:
|
||||
- ./config:/etc/netbox/config:z,ro
|
||||
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
+58
-32
@@ -1,63 +1,89 @@
|
||||
---
|
||||
# based on https://github.com/netbox-community/netbox-docker/blob/5.0.1/docker-compose.yml
|
||||
services:
|
||||
netbox: &netbox
|
||||
image: netboxcommunity/netbox:${VERSION-v4.0.6}
|
||||
image: docker.io/netboxcommunity/netbox:${VERSION-v4.6.3}
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
- netbox-worker
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
env_file: env/netbox.env
|
||||
user: 'unit:root'
|
||||
user: "netbox:root"
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:8080/login/ || exit 1
|
||||
start_period: 90s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
volumes:
|
||||
- ./configuration:/etc/netbox/config:z,ro
|
||||
- netbox-media-files:/opt/netbox/netbox/media:z
|
||||
- ./config:/etc/netbox/config:z,ro
|
||||
- netbox-media-files:/opt/netbox/netbox/media:rw
|
||||
- netbox-reports-files:/opt/netbox/netbox/reports:rw
|
||||
- netbox-scripts-files:/opt/netbox/netbox/scripts:rw
|
||||
netbox-worker:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
netbox:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /opt/netbox/venv/bin/python
|
||||
- /opt/netbox/netbox/manage.py
|
||||
- rqworker
|
||||
netbox-housekeeping:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command:
|
||||
- /opt/netbox/housekeeping.sh
|
||||
- /opt/netbox/venv/bin/python
|
||||
- /opt/netbox/netbox/manage.py
|
||||
- rqworker
|
||||
healthcheck:
|
||||
test: ps -aux | grep -v grep | grep -q rqworker || exit 1
|
||||
start_period: 20s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
|
||||
# postgres
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
image: docker.io/postgres:18-alpine
|
||||
healthcheck:
|
||||
test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER
|
||||
start_period: 20s
|
||||
timeout: 30s
|
||||
interval: 10s
|
||||
retries: 5
|
||||
env_file: env/postgres.env
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
- netbox-postgres:/var/lib/postgresql
|
||||
|
||||
# redis
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
image: docker.io/valkey/valkey:9.0-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
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
healthcheck: &redis-healthcheck
|
||||
test: '[ $$(valkey-cli --pass "$${REDIS_PASSWORD}" ping) = ''PONG'' ]'
|
||||
start_period: 5s
|
||||
timeout: 3s
|
||||
interval: 1s
|
||||
retries: 5
|
||||
env_file: env/redis.env
|
||||
volumes:
|
||||
- netbox-redis-data:/data
|
||||
- netbox-redis-data:/data
|
||||
redis-cache:
|
||||
image: redis:7-alpine
|
||||
image: docker.io/valkey/valkey:9.0-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- valkey-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
healthcheck: *redis-healthcheck
|
||||
env_file: env/redis-cache.env
|
||||
volumes:
|
||||
- netbox-redis-cache-data:/data
|
||||
|
||||
volumes:
|
||||
netbox-media-files:
|
||||
driver: local
|
||||
netbox-postgres-data:
|
||||
netbox-postgres:
|
||||
driver: local
|
||||
netbox-redis-cache-data:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
netbox-reports-files:
|
||||
driver: local
|
||||
netbox-scripts-files:
|
||||
driver: local
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
coverage==7.5.4
|
||||
unittest-xml-reporting==3.2.0
|
||||
@@ -0,0 +1,66 @@
|
||||
Anchore
|
||||
APIs
|
||||
Caddy
|
||||
Checkmarx
|
||||
Cobertura
|
||||
Dependabot
|
||||
Docker-composition
|
||||
Dockerfile
|
||||
DTDs
|
||||
ESLint
|
||||
Gravatar
|
||||
Jira
|
||||
JMeter
|
||||
JUnit
|
||||
NCover
|
||||
Nginx
|
||||
OJAudit
|
||||
OpenShift
|
||||
PDFs
|
||||
Pydantic
|
||||
Robocop
|
||||
Snyk
|
||||
Trello
|
||||
Trivy
|
||||
UUIDs
|
||||
Wekan
|
||||
[Hh]ostname
|
||||
[Uu]nmerged
|
||||
asyncio
|
||||
autoformatting
|
||||
breakpoint
|
||||
clearable
|
||||
cloc
|
||||
discoverability
|
||||
donut
|
||||
errored
|
||||
favicon
|
||||
fixme
|
||||
hostnames?
|
||||
hotspots?
|
||||
lookback
|
||||
misconfigured
|
||||
mypy
|
||||
namespace
|
||||
npm
|
||||
parameterizable
|
||||
phpldapadmin
|
||||
[Pp]erformancetest
|
||||
severities
|
||||
sparkline
|
||||
subfolders
|
||||
submenus
|
||||
suppressions
|
||||
todo
|
||||
tooltips?
|
||||
tracebacks?
|
||||
unencrypted
|
||||
unicode
|
||||
unmerged
|
||||
upvotes
|
||||
url
|
||||
xml
|
||||
NetBox
|
||||
NetBox SLM
|
||||
Mart
|
||||
Visser
|
||||
@@ -0,0 +1,3 @@
|
||||
docker composition
|
||||
Docker composition
|
||||
docker-composition
|
||||
+24
-3
@@ -1,18 +1,39 @@
|
||||
"""
|
||||
Copyright 2022-2025 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.
|
||||
"""
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
__version__ = "1.7.0"
|
||||
__version__ = "1.9.0"
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = "netbox_slm"
|
||||
verbose_name = "Software Lifecycle Management"
|
||||
description = "Software Lifecycle Management Netbox Plugin."
|
||||
version = __version__
|
||||
description = "Software Lifecycle Management Netbox Plugin."
|
||||
author = "ICTU"
|
||||
author_email = "open-source-projects@ictu.nl"
|
||||
base_url = "slm"
|
||||
required_settings = []
|
||||
default_settings = {"version_info": False}
|
||||
default_settings = {
|
||||
"top_level_menu": True,
|
||||
"link_cluster_installations": "right",
|
||||
"link_device_installations": "right",
|
||||
"link_virtualmachine_installations": "right",
|
||||
}
|
||||
|
||||
|
||||
config = SLMConfig
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
@@ -17,6 +17,7 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
@@ -39,8 +40,8 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
@@ -49,6 +50,7 @@ class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"tags",
|
||||
@@ -60,14 +62,12 @@ class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
brief_fields = ("id", "display", "url", "name", "description")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj.manufacturer} - {obj}"
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail"
|
||||
)
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
@@ -86,15 +86,15 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url", "name")
|
||||
brief_fields = ("id", "display", "url")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
@@ -103,6 +103,7 @@ class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
@@ -117,7 +118,7 @@ class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url", "name")
|
||||
brief_fields = ("id", "display", "url", "name", "description")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
@@ -14,4 +14,5 @@ router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
+10
-9
@@ -13,37 +13,38 @@ from netbox_slm.filtersets import (
|
||||
SoftwareProductInstallationFilterSet,
|
||||
SoftwareLicenseFilterSet,
|
||||
)
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
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()
|
||||
queryset = SoftwareProduct.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilterSet
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
queryset = SoftwareProductVersion.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilterSet
|
||||
|
||||
|
||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
queryset = SoftwareProductInstallation.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilterSet
|
||||
|
||||
|
||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
queryset = SoftwareLicense.objects.all().order_by("id")
|
||||
serializer_class = SoftwareLicenseSerializer
|
||||
filterset_class = SoftwareLicenseFilterSet
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
from django.db.models import Q
|
||||
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
|
||||
|
||||
from dcim.models import Device, Manufacturer
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
SoftwareReleaseTypes,
|
||||
)
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
|
||||
manufacturer_id = ModelMultipleChoiceFilter(queryset=Manufacturer.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = tuple()
|
||||
@@ -27,9 +41,25 @@ class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductVersion instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
filename = CharFilter(lookup_expr="icontains")
|
||||
|
||||
release_type = MultipleChoiceFilter(choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
manufacturer_id = ModelMultipleChoiceFilter(
|
||||
field_name="software_product__manufacturer",
|
||||
queryset=Manufacturer.objects.all(),
|
||||
)
|
||||
|
||||
software_product_id = ModelMultipleChoiceFilter(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
label="Software Product",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("software_product",)
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -37,6 +67,7 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
@@ -47,9 +78,21 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductInstallation instances."""
|
||||
|
||||
device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all())
|
||||
virtualmachine_id = ModelMultipleChoiceFilter(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
label="Virtual Machine",
|
||||
)
|
||||
cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all())
|
||||
software_product_id = ModelMultipleChoiceFilter(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
label="Software Product",
|
||||
)
|
||||
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("software_product",)
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -67,9 +110,22 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareLicense instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
type = CharFilter(lookup_expr="icontains")
|
||||
spdx_expression = CharFilter(lookup_expr="icontains", label="SPDX expression")
|
||||
stored_location = CharFilter(lookup_expr="icontains")
|
||||
|
||||
software_product_id = ModelMultipleChoiceFilter(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
label="Software Product",
|
||||
)
|
||||
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
|
||||
installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = tuple()
|
||||
fields = ("support",)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -77,6 +133,7 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
| Q(installation__device__name__icontains=value)
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
from .software_license import SoftwareLicenseForm, SoftwareLicenseFilterForm
|
||||
from .software_product import SoftwareProductForm, SoftwareProductFilterForm
|
||||
from .software_product_installation import SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm
|
||||
from .software_product_version import SoftwareProductVersionForm, SoftwareProductVersionFilterForm
|
||||
from .software_license import (
|
||||
SoftwareLicenseForm,
|
||||
SoftwareLicenseFilterForm,
|
||||
SoftwareLicenseBulkImportForm,
|
||||
SoftwareLicenseBulkEditForm,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductForm,
|
||||
SoftwareProductFilterForm,
|
||||
SoftwareProductBulkImportForm,
|
||||
SoftwareProductBulkEditForm,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationForm,
|
||||
SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationBulkImportForm,
|
||||
SoftwareProductInstallationBulkEditForm,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionForm,
|
||||
SoftwareProductVersionFilterForm,
|
||||
SoftwareProductVersionBulkImportForm,
|
||||
SoftwareProductVersionBulkEditForm,
|
||||
)
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
from django.forms import DateField
|
||||
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField, LaxURLField
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
spdx_license_names,
|
||||
)
|
||||
from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
LaxURLField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelect, DatePicker
|
||||
|
||||
|
||||
class SoftwareLicenseForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareLicense object."""
|
||||
|
||||
comments = CommentField()
|
||||
|
||||
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression")
|
||||
stored_location_url = LaxURLField(required=False)
|
||||
start_date = DateField(required=False, widget=DatePicker())
|
||||
expiration_date = DateField(required=False, widget=DatePicker())
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
label="Software Product",
|
||||
)
|
||||
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",
|
||||
},
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
installation = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
@@ -34,26 +46,23 @@ class SoftwareLicenseForm(NetBoxModelForm):
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
||||
),
|
||||
query_params={
|
||||
"software_product": "$software_product",
|
||||
},
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
start_date = DateField(required=False, widget=DatePicker())
|
||||
expiration_date = DateField(required=False, widget=DatePicker())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"support",
|
||||
"license_amount",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
@@ -63,5 +72,128 @@ class SoftwareLicenseForm(NetBoxModelForm):
|
||||
|
||||
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (FieldSet(None, ("q", "tag")),)
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet(
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"support",
|
||||
"software_product_id",
|
||||
"version_id",
|
||||
"installation_id",
|
||||
),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
name = CharField(required=False)
|
||||
description = CharField(required=False)
|
||||
type = CharField(required=False)
|
||||
spdx_expression = CharField(required=False, label="SPDX expression")
|
||||
stored_location = CharField(required=False)
|
||||
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
|
||||
software_product_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
version_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
label="Version",
|
||||
)
|
||||
installation_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
required=False,
|
||||
label="Installation",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
|
||||
support = NullBooleanField(required=False, help_text="Support (values other than 'True' and 'False' are ignored)")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"support",
|
||||
"license_amount",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"support",
|
||||
"license_amount",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
),
|
||||
)
|
||||
nullable_fields = (
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"support",
|
||||
"license_amount",
|
||||
"version",
|
||||
"installation",
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
type = CharField(required=False)
|
||||
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression")
|
||||
stored_location = CharField(required=False)
|
||||
stored_location_url = LaxURLField(required=False)
|
||||
start_date = DateField(required=False, widget=DatePicker())
|
||||
expiration_date = DateField(required=False, widget=DatePicker())
|
||||
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||
license_amount = IntegerField(required=False, min_value=0)
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params=dict(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=dict(software_product="$software_product"),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from django.forms import CharField
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
comments = CommentField()
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
@@ -28,5 +33,43 @@ class SoftwareProductForm(NetBoxModelForm):
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (FieldSet(None, ("q", "tag")),)
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("name", "description", "manufacturer_id"),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
name = CharField(required=False)
|
||||
description = CharField(required=False)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label="Manufacturer",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductBulkImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (FieldSet("manufacturer"),)
|
||||
nullable_fields = ("manufacturer", "comments")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
@@ -2,34 +2,39 @@ from django.forms import ValidationError
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from dcim.models import Device
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelect
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
comments = CommentField()
|
||||
|
||||
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
||||
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label="Virtual Machine",
|
||||
)
|
||||
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")}),
|
||||
label="Software Product",
|
||||
)
|
||||
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",
|
||||
},
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -57,5 +62,77 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
|
||||
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (FieldSet(None, ("q", "tag")),)
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("device_id", "virtualmachine_id", "cluster_id", "software_product_id", "version_id"),
|
||||
)
|
||||
selector_fields = ("filter_id", "q")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label="Device",
|
||||
)
|
||||
virtualmachine_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label="Virtual Machine",
|
||||
)
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label="Cluster",
|
||||
)
|
||||
software_product_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
version_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
label="Version",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (FieldSet("device", "virtualmachine", "cluster", "software_product", "version"),)
|
||||
nullable_fields = ("device", "virtualmachine", "cluster", "comments")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label="Virtual Machine",
|
||||
)
|
||||
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
from django.forms import DateField
|
||||
from django.forms import DateField, CharField, MultipleChoiceField
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelect, DatePicker
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
|
||||
comments = CommentField()
|
||||
|
||||
release_date = DateField(required=False, widget=DatePicker())
|
||||
@@ -18,6 +22,8 @@ class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
label="Software Product",
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
)
|
||||
|
||||
@@ -25,6 +31,70 @@ class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"file_link",
|
||||
"release_type",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("name", "description", "filename", "release_type", "manufacturer_id", "software_product_id"),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
name = CharField(required=False)
|
||||
description = CharField(required=False)
|
||||
filename = CharField(required=False)
|
||||
|
||||
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label="Manufacturer",
|
||||
)
|
||||
software_product_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm):
|
||||
release_type = CharField(help_text=f"Release type (possible values: {SoftwareReleaseTypes.values})")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"release_type",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
@@ -33,12 +103,24 @@ class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"file_link",
|
||||
"release_type",
|
||||
"software_product",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
),
|
||||
)
|
||||
nullable_fields = ("release_date", "documentation_url", "end_of_support", "filename", "file_checksum", "file_link")
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (FieldSet(None, ("q", "tag")),)
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
release_date = DateField(required=False, widget=DatePicker())
|
||||
documentation_url = CharField(required=False)
|
||||
end_of_support = DateField(required=False, widget=DatePicker())
|
||||
filename = CharField(required=False)
|
||||
file_checksum = CharField(required=False)
|
||||
file_link = CharField(required=False)
|
||||
|
||||
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label="Software Product",
|
||||
)
|
||||
|
||||
@@ -24,6 +24,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
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.'),
|
||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-17 21:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("netbox_slm", "0008_software_release_types_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="softwareproductversion",
|
||||
name="description",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-24 18:03
|
||||
|
||||
import netbox_slm.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("netbox_slm", "0009_softwareproductversion_description"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="softwarelicense",
|
||||
name="spdx_expression",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
max_length=64,
|
||||
null=True,
|
||||
validators=[netbox_slm.models.validate_spdx_expression],
|
||||
),
|
||||
),
|
||||
]
|
||||
+27
-22
@@ -1,11 +1,15 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import safestring
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from license_expression import Licensing, get_spdx_licensing
|
||||
|
||||
from netbox.models import NetBoxModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import EnhancedURLValidator
|
||||
|
||||
spdx_licensing: Licensing = get_spdx_licensing()
|
||||
|
||||
|
||||
class LaxURLField(models.URLField):
|
||||
"""
|
||||
@@ -33,16 +37,10 @@ class SoftwareProduct(NetBoxModel):
|
||||
|
||||
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"
|
||||
search_target = reverse(
|
||||
"plugins:netbox_slm:softwareproductinstallation_list", query={"software_product_id": self.pk}
|
||||
)
|
||||
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||
|
||||
|
||||
class SoftwareReleaseTypes(models.TextChoices):
|
||||
@@ -56,6 +54,7 @@ class SoftwareProductVersion(NetBoxModel):
|
||||
name = models.CharField(max_length=64)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
release_date = models.DateField(null=True, blank=True)
|
||||
documentation_url = LaxURLField(max_length=1024, null=True, blank=True)
|
||||
end_of_support = models.DateField(null=True, blank=True)
|
||||
@@ -84,16 +83,8 @@ class SoftwareProductVersion(NetBoxModel):
|
||||
|
||||
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"
|
||||
)
|
||||
search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list", query={"version_id": self.pk})
|
||||
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||
|
||||
|
||||
class SoftwareProductInstallation(NetBoxModel):
|
||||
@@ -116,7 +107,7 @@ class SoftwareProductInstallation(NetBoxModel):
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name="%(app_label)s_%(class)s_platform",
|
||||
check=(
|
||||
condition=(
|
||||
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)
|
||||
@@ -140,12 +131,26 @@ class SoftwareProductInstallation(NetBoxModel):
|
||||
return "cluster"
|
||||
|
||||
|
||||
def spdx_license_names():
|
||||
names = [(item[0], item[0]) for item in spdx_licensing.known_symbols.items() if not item[1].is_exception]
|
||||
names.sort()
|
||||
names.insert(0, ("", "---------")) # set default value
|
||||
return names
|
||||
|
||||
|
||||
def validate_spdx_expression(value):
|
||||
expression_info = spdx_licensing.validate(value)
|
||||
if expression_info.errors:
|
||||
raise ValidationError(f"{value} is not a known SPDX license expression")
|
||||
|
||||
|
||||
class SoftwareLicense(NetBoxModel):
|
||||
name = models.CharField(max_length=128)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
type = models.CharField(max_length=128)
|
||||
spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression])
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
|
||||
from . import SLMConfig
|
||||
|
||||
menu_items = (
|
||||
slm_items = (
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwareproduct_list",
|
||||
link_text="Software Products",
|
||||
@@ -12,6 +13,12 @@ menu_items = (
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproduct"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
@@ -25,6 +32,12 @@ menu_items = (
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproductversion"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
@@ -38,6 +51,12 @@ menu_items = (
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproductinstallation"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
@@ -51,6 +70,22 @@ menu_items = (
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwarelicense"],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if get_plugin_config("netbox_slm", "top_level_menu"):
|
||||
menu = PluginMenu(
|
||||
label="Software Lifecycle",
|
||||
groups=((SLMConfig.verbose_name, slm_items),),
|
||||
icon_class="mdi mdi-content-save",
|
||||
)
|
||||
else:
|
||||
# auto imported by default PluginConfig.menu_items = navigation.menu_items
|
||||
menu_items = slm_items
|
||||
|
||||
+16
-31
@@ -20,8 +20,8 @@ class SoftwareProductTable(NetBoxTable):
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
@@ -29,16 +29,9 @@ class SoftwareProductTable(NetBoxTable):
|
||||
"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(
|
||||
@@ -52,7 +45,7 @@ class SoftwareProductVersionTable(NetBoxTable):
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
||||
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count")
|
||||
|
||||
@@ -63,6 +56,7 @@ class SoftwareProductVersionTable(NetBoxTable):
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"release_date",
|
||||
@@ -74,18 +68,12 @@ class SoftwareProductVersionTable(NetBoxTable):
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"name",
|
||||
"installations",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
@@ -98,14 +86,10 @@ 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)
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
|
||||
@@ -114,9 +98,9 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
@@ -125,6 +109,7 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
@@ -144,9 +129,6 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
||||
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."""
|
||||
@@ -155,9 +137,11 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
name = tables.LinkColumn()
|
||||
|
||||
type = tables.Column()
|
||||
spdx_expression = tables.Column(verbose_name="SPDX expression")
|
||||
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
|
||||
|
||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
installation = tables.Column(accessor="installation", linkify=True)
|
||||
|
||||
@@ -170,9 +154,11 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
@@ -183,14 +169,13 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"expiration_date",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"installation",
|
||||
"expiration_date",
|
||||
"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,36 @@
|
||||
from netbox.plugins import PluginTemplateExtension, get_plugin_config
|
||||
|
||||
installations = dict(
|
||||
device=get_plugin_config("netbox_slm", "link_device_installations"),
|
||||
cluster=get_plugin_config("netbox_slm", "link_cluster_installations"),
|
||||
virtualmachine=get_plugin_config("netbox_slm", "link_virtualmachine_installations"),
|
||||
)
|
||||
|
||||
|
||||
class InstallationsCard(PluginTemplateExtension):
|
||||
models = ["dcim.device", "virtualization.cluster", "virtualization.virtualmachine"]
|
||||
|
||||
def get_object_name(self):
|
||||
object_model_meta = self.context["object"]._meta # one of the self.models defined above
|
||||
return object_model_meta.model_name
|
||||
|
||||
def render_card(self):
|
||||
return self.render(
|
||||
"netbox_slm/installations_card_include.html",
|
||||
extra_context={
|
||||
"search_obj": self.get_object_name(),
|
||||
},
|
||||
)
|
||||
|
||||
def left_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "left" else ""
|
||||
|
||||
def right_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "right" else ""
|
||||
|
||||
def full_width_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "full" else ""
|
||||
|
||||
|
||||
# auto imported by default PluginConfig.template_extensions = template_content.template_extensions
|
||||
template_extensions = [InstallationsCard]
|
||||
@@ -0,0 +1,21 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
Installations
|
||||
{% if perms.netbox_slm.add_softwareproductinstallation %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add an installation
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if search_obj == "cluster" %}
|
||||
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" cluster_id=object.pk %}
|
||||
{% elif search_obj == "device" %}
|
||||
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" device_id=object.pk %}
|
||||
{% elif search_obj == "virtualmachine" %}
|
||||
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" virtualmachine_id=object.pk %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -24,6 +24,22 @@
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">SPDX expression</th>
|
||||
<td>{{ object.spdx_expression }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object.installation|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stored location</th>
|
||||
{% if object.stored_location_url %}
|
||||
@@ -40,14 +56,6 @@
|
||||
<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">Support</th>
|
||||
<td>{{ object.support }}</td>
|
||||
@@ -56,21 +64,17 @@
|
||||
<th scope="row">License amount</th>
|
||||
<td>{{ object.license_amount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object.installation }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -22,31 +22,55 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ object.manufacturer }}</td>
|
||||
<td>{{ object.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
{% for version in object.softwareproduct_versions.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
||||
{% for version in object.softwareproductversion_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
|
||||
{{ version }}
|
||||
</a>
|
||||
{% empty %}
|
||||
n/a
|
||||
{% endfor %}
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>
|
||||
{% for installation in object.softwareproductinstallation_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
|
||||
{{ installation }}
|
||||
</a>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Licenses</th>
|
||||
<td>
|
||||
{% for license in object.softwarelicense_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
|
||||
{{ license }}
|
||||
</a>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,38 +15,50 @@
|
||||
{% if object.device %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ object.device }}</td>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
{% elif object.virtualmachine %}
|
||||
<tr>
|
||||
<th scope="row">Virtualmachine</th>
|
||||
<td>{{ object.virtualmachine }}</td>
|
||||
<th scope="row">Virtual Machine</th>
|
||||
<td>{{ object.virtualmachine|linkify }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">Cluster</th>
|
||||
<td>{{ object.cluster }}</td>
|
||||
<td>{{ object.cluster|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
<td>{{ object.software_product|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version }}</td>
|
||||
<td>{{ object.version|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Licenses</th>
|
||||
<td>
|
||||
{% for license in object.softwarelicense_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
|
||||
{{ license }}
|
||||
</a>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ object.software_product.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Release date</th>
|
||||
<td>{{ object.release_date }}</td>
|
||||
@@ -50,19 +62,39 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>{{ object.get_installation_count }}</td>
|
||||
<td>
|
||||
{% for installation in object.softwareproductinstallation_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
|
||||
{{ installation }}
|
||||
</a>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Licenses</th>
|
||||
<td>
|
||||
{% for license in object.softwarelicense_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
|
||||
{{ license }}
|
||||
</a>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class SlmBaseTestCase(TestCase):
|
||||
p_name: str = "test product"
|
||||
v_name: str = "test version"
|
||||
l_name: str = "test license"
|
||||
test_url: str = "https://github.com/ICTU/netbox_slm"
|
||||
vm: VirtualMachine
|
||||
software_product: SoftwareProduct
|
||||
software_product_version: SoftwareProductVersion
|
||||
software_product_installation: SoftwareProductInstallation
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
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")
|
||||
cls.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
|
||||
cls.vm = VirtualMachine.objects.create(name="test VM")
|
||||
cluster_type = ClusterType.objects.create(name="test cluster type")
|
||||
cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
|
||||
|
||||
cls.software_product = SoftwareProduct.objects.create(name=cls.p_name)
|
||||
cls.software_product_version = SoftwareProductVersion.objects.create(
|
||||
name=cls.v_name, software_product=cls.software_product
|
||||
)
|
||||
cls.software_product_installation = SoftwareProductInstallation.objects.create(
|
||||
virtualmachine=cls.vm, software_product=cls.software_product, version=cls.software_product_version
|
||||
)
|
||||
cls.software_license = SoftwareLicense.objects.create(
|
||||
name=cls.l_name,
|
||||
software_product=cls.software_product,
|
||||
version=cls.software_product_version,
|
||||
installation=cls.software_product_installation,
|
||||
stored_location_url=cls.test_url,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
SoftwareLicense.objects.all().delete()
|
||||
SoftwareProductInstallation.objects.all().delete()
|
||||
SoftwareProductVersion.objects.all().delete()
|
||||
SoftwareProduct.objects.all().delete()
|
||||
Cluster.objects.all().delete()
|
||||
ClusterType.objects.all().delete()
|
||||
VirtualMachine.objects.all().delete()
|
||||
Device.objects.all().delete()
|
||||
Site.objects.all().delete()
|
||||
DeviceRole.objects.all().delete()
|
||||
DeviceType.objects.all().delete()
|
||||
Manufacturer.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
@@ -0,0 +1,51 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .base import SlmBaseTestCase
|
||||
|
||||
|
||||
class FunctionalTestCase(SlmBaseTestCase):
|
||||
"""Functional test cases that require a client and user with permissions"""
|
||||
|
||||
def setUp(self):
|
||||
test_user, _ = get_user_model().objects.get_or_create(username="test", is_superuser=True)
|
||||
self.client.force_login(test_user)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
get_user_model().objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
def test_template_content(self):
|
||||
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
|
||||
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
|
||||
self.assertContains(cluster_response, "</span> Add an installation")
|
||||
|
||||
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
|
||||
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
|
||||
self.assertContains(device_response, "</span> Add an installation")
|
||||
|
||||
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
|
||||
self.assertTemplateUsed(vm_response, "netbox_slm/installations_card_include.html")
|
||||
self.assertContains(vm_response, f'<a href="/plugins/slm/installations/add/?virtualmachine={self.vm.pk}"')
|
||||
|
||||
def test_setting_skip_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_virtualmachine_installations=None)))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"virtualmachine": None}):
|
||||
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
|
||||
self.assertTemplateNotUsed(vm_response, "netbox_slm/installations_card_include.html")
|
||||
|
||||
def test_setting_left_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_device_installations="left")))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"device": "left"}):
|
||||
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
|
||||
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
|
||||
self.assertContains(device_response, "</span> Add an installation")
|
||||
|
||||
def test_setting_full_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_cluster_installations="full")))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"cluster": "full"}):
|
||||
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
|
||||
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
|
||||
self.assertContains(cluster_response, "</span> Add an installation")
|
||||
@@ -1,40 +1,8 @@
|
||||
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
|
||||
from .base import SlmBaseTestCase
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
class ModelTestCase(SlmBaseTestCase):
|
||||
"""Test basic model functionality and custom overrides"""
|
||||
|
||||
def test_model_name(self):
|
||||
self.assertEqual(self.p_name, str(self.software_product))
|
||||
@@ -43,15 +11,28 @@ class ModelTestCase(TestCase):
|
||||
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())
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/software-products/{self.software_product.pk}/", self.software_product.get_absolute_url()
|
||||
)
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/versions/{self.software_product_version.pk}/",
|
||||
self.software_product_version.get_absolute_url(),
|
||||
)
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/installations/{self.software_product_installation.pk}/",
|
||||
self.software_product_installation.get_absolute_url(),
|
||||
)
|
||||
self.assertEqual(f"/plugins/slm/licenses/{self.software_license.pk}/", 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())
|
||||
self.assertEqual(
|
||||
f"<a href='/plugins/slm/installations/?software_product_id={self.software_product.pk}'>1</a>",
|
||||
self.software_product.get_installation_count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
f"<a href='/plugins/slm/installations/?version_id={self.software_product_version.pk}'>1</a>",
|
||||
self.software_product_version.get_installation_count(),
|
||||
)
|
||||
|
||||
def test_product_installation_methods(self):
|
||||
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
||||
|
||||
+11
-92
@@ -1,96 +1,15 @@
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from netbox_slm import views # noqa F401
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
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/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/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/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/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},
|
||||
),
|
||||
path("software-products/", include(get_model_urls("netbox_slm", "softwareproduct", detail=False))),
|
||||
path("software-products/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproduct"))),
|
||||
path("versions/", include(get_model_urls("netbox_slm", "softwareproductversion", detail=False))),
|
||||
path("versions/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductversion"))),
|
||||
path("installations/", include(get_model_urls("netbox_slm", "softwareproductinstallation", detail=False))),
|
||||
path("installations/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductinstallation"))),
|
||||
path("licenses/", include(get_model_urls("netbox_slm", "softwarelicense", detail=False))),
|
||||
path("licenses/<int:pk>/", include(get_model_urls("netbox_slm", "softwarelicense"))),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from .software_license import (
|
||||
SoftwareLicenseEditView,
|
||||
SoftwareLicenseDeleteView,
|
||||
SoftwareLicenseBulkDeleteView,
|
||||
SoftwareLicenseBulkImportView,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductListView,
|
||||
@@ -11,6 +12,7 @@ from .software_product import (
|
||||
SoftwareProductEditView,
|
||||
SoftwareProductDeleteView,
|
||||
SoftwareProductBulkDeleteView,
|
||||
SoftwareProductBulkImportView,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationListView,
|
||||
@@ -18,6 +20,7 @@ from .software_product_installation import (
|
||||
SoftwareProductInstallationEditView,
|
||||
SoftwareProductInstallationDeleteView,
|
||||
SoftwareProductInstallationBulkDeleteView,
|
||||
SoftwareProductInstallationBulkImportView,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionListView,
|
||||
@@ -25,4 +28,5 @@ from .software_product_version import (
|
||||
SoftwareProductVersionEditView,
|
||||
SoftwareProductVersionDeleteView,
|
||||
SoftwareProductVersionBulkDeleteView,
|
||||
SoftwareProductVersionBulkImportView,
|
||||
)
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareLicense
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "list", path="", detail=False)
|
||||
class SoftwareLicenseListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareLicenses."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
filterset_form = forms.SoftwareLicenseFilterForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense)
|
||||
class SoftwareLicenseView(generic.ObjectView):
|
||||
"""Display SoftwareLicense details"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "add", detail=False)
|
||||
@register_model_view(SoftwareLicense, "edit")
|
||||
class SoftwareLicenseEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareLicense instance."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
form = forms.SoftwareLicenseForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "delete")
|
||||
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareLicense instance"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_import", detail=False)
|
||||
class SoftwareLicenseBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
model_form = forms.SoftwareLicenseBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareLicenseBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
form = forms.SoftwareLicenseBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "list", path="", detail=False)
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct)
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@@ -22,19 +21,34 @@ class SoftwareProductView(generic.ObjectView):
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "add", detail=False)
|
||||
@register_model_view(SoftwareProduct, "edit")
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = forms.SoftwareProductForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "delete")
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_import", detail=False)
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = forms.SoftwareProductBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
table = tables.SoftwareProductTable
|
||||
form = forms.SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductInstallation
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "list", path="", detail=False)
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation)
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "add", detail=False)
|
||||
@register_model_view(SoftwareProductInstallation, "edit")
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = forms.SoftwareProductInstallationForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "delete")
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_import", detail=False)
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = forms.SoftwareProductInstallationBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
form = forms.SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductVersion
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "list", path="", detail=False)
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion)
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@@ -22,19 +21,34 @@ class SoftwareProductVersionView(generic.ObjectView):
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "add", detail=False)
|
||||
@register_model_view(SoftwareProductVersion, "edit")
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = forms.SoftwareProductVersionForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "delete")
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_import", detail=False)
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = forms.SoftwareProductVersionBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
form = forms.SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
+30
-8
@@ -13,31 +13,44 @@ authors = [
|
||||
{name = "ICTU", email = "open-source-projects@ictu.nl"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Plugins",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"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.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
]
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"license-expression == 30.4.4",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
build = [
|
||||
"build == 1.2.1",
|
||||
"setuptools == 70.3.0",
|
||||
"twine == 5.1.1",
|
||||
"build == 1.5.0",
|
||||
"setuptools == 82.0.1",
|
||||
"twine == 6.2.0",
|
||||
]
|
||||
tools = [
|
||||
"ruff == 0.5.1",
|
||||
ci = [
|
||||
"coverage == 7.14.3",
|
||||
"ruff == 0.15.19",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/ICTU/netbox_slm/"
|
||||
Changelog = "https://github.com/ICTU/netbox_slm/blob/master/CHANGELOG.md"
|
||||
Issues = "https://github.com/ICTU/netbox_slm/issues/"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "netbox_slm.__version__"}
|
||||
|
||||
@@ -46,9 +59,18 @@ include = ["netbox_slm"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
target-version = "py312"
|
||||
src = ["netbox_slm"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle
|
||||
"F", # Pyflakes
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"SIM", # flake8-simplify
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = [
|
||||
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
sonar.organization=ictu
|
||||
sonar.projectKey=ICTU_netbox_slm
|
||||
sonar.projectName=netbox-slm
|
||||
sonar.projectVersion=1.4
|
||||
sonar.projectVersion=1.9.0
|
||||
|
||||
# Path is relative to the sonar-project.properties file
|
||||
sonar.sources=netbox_slm
|
||||
sonar.python.version=3.9, 3.10, 3.11
|
||||
sonar.exclusions=netbox_slm/tests/**
|
||||
sonar.tests=netbox_slm/tests
|
||||
sonar.python.version=3.11, 3.12, 3.13, 3.14
|
||||
|
||||
# Unit tests
|
||||
sonar.python.xunit.reportPath=ci/reports/junit.xml
|
||||
sonar.python.coverage.reportPaths=ci/reports/coverage.xml
|
||||
# Python test reports
|
||||
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
|
||||
|
||||
Reference in New Issue
Block a user