34 Commits
Author SHA1 Message Date
wkootandArtem Kotik f84a6bde2a Version bump to NetBox v4.6.3 (#82)
* Add support for NetBox 4.6
* Test NetBox v4.6.3 and Python 3.14

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
Co-authored-by: Artem Kotik <artem.i.kotik@ringcentral.com>
2026-06-25 18:06:27 +02:00
wkoot 811490e429 Add zizmor scan action (#79) 2026-06-19 14:12:46 +02:00
wkootandmikedekker 7ce718ff9d Fix filters with default order by ID (#78)
Closes #75

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
Co-authored-by: mikedekker <mike@dekker.me>
2025-12-18 13:30:38 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9e87afcde7 Bump SonarSource/sonarqube-scan-action from 5 to 6 in /.github/workflows (#74)
Bumps [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 5 to 6.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: SonarSource/sonarqube-scan-action
  dependency-version: '6'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 23:17:19 +02:00
wkoot e03195c9b1 Bump sonarqube-scan-action to v5 (#73) 2025-09-02 09:49:18 +02:00
wkoot 59730a5184 Update CI spellchecker (#72) 2025-06-04 11:31:05 +02:00
wkoot 69594bc33a Release version 1.8.2
Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-06-02 22:11:32 +02:00
Nate Cohen 766ecfc25e Fix permission check for adding software product installations (#71) 2025-06-02 22:05:43 +02:00
wkoot d773a407a2 Fix filtering on foreign objects (#67)
Closes #65

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-04-11 12:29:03 +02:00
wkoot 175a01b257 Release plugin version 1.8.0 (#64)
Closes #63

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-25 20:13:25 +01:00
wkoot 334a76e640 Add SPDX license expressions (#62)
Closes #47

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-25 18:37:00 +01:00
wkoot ef45789b30 Show SLM installations on native objects (#61)
Closes #49

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-24 16:57:18 +01:00
wkoot 4bd02ab6f9 Add bulk import and bulk edit (#60)
Closes #4

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-19 23:07:47 +01:00
wkoot 410b8f6617 Render SoftwareProduct manufacturer consistently (#59)
Closes #58

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-17 23:01:51 +01:00
wkoot 306d853a16 Fix unencoded search link of installations table cells (#53)
Closes #48

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-13 22:58:22 +01:00
wkoot 193f04a208 Implement list filtering forms (#57)
Closes #54

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-13 22:16:32 +01:00
wkoot c3ed952b5f Fix model detail view listing of linked objects (#56)
Closes #55

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-12 23:30:55 +01:00
wkoot 87fdeb1c24 Version bump to NetBox v4.2.3 and friends (#52)
Closes #51

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
2025-02-11 20:41:03 +01:00
wkootandEric Hester a0a98bb407 Implement bulk imports and edits for SLM models (#46)
Partially implements #4
Based on @erichester76's https://github.com/ICTU/netbox_slm/pull/44

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
Co-authored-by: Eric Hester <eric.hester@umbrella.tech>
2024-10-14 13:41:22 +02:00
wkootandEric Hester 3539539c4e Implement top_level_menu toggle (#45)
Closes #42
Based on @erichester76's https://github.com/ICTU/netbox_slm/pull/44

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
Co-authored-by: Eric Hester <eric.hester@umbrella.tech>
2024-10-14 11:13:58 +02:00
wkoot 72646a6287 Update PluginMenuItem permissions and remove bulk import/edit (#41)
Closes #37
2024-07-09 23:00:58 +02:00
wkoot 86215f43a6 Prepare for upgrade to NetBox v4.0.6 (#40)
Closes #38
2024-07-09 22:15:58 +02:00
wkoot 230b3b5115 Add new fields to models and upgrade to NetBox v3.7.2 (#36)
Closes #10, #26, #35
2024-02-13 12:11:49 +01:00
wkoot 1411e3a5cb Update MANIFEST.in, so all files are included (#34)
Closes #32
2023-12-08 10:14:23 +01:00
wkoot d082af4a9b Add Installation foreign key to VM Cluster (#29)
Closes #17
2023-10-04 18:23:08 +02:00
wkoot 2501e41b7c Add 'stored_location_url' to SoftwareLicense (#28)
Closes #27
2023-09-22 02:42:02 +02:00
wkootandManuel Hartung aa41f8a402 Assorted fixes (#25)
* Updated imports for Netbox 3.6.1

Closes #21 and #23

* Changed Fieldtypes of Start and Expiration Date

Closes #22

* Bump to NetBox v3.6.1

* Add pyproject.toml and python workflow

---------

Co-authored-by: Manuel Hartung <manuel.hartung@atos.net>
2023-09-15 09:41:36 +02:00
wkoot 6c512e0e9b Add SoftwareLicense (#20) 2023-06-30 15:11:28 +02:00
wkoot 5b96496b46 Replace NetBoxModelCSVForm with NetBoxModelImportForm (#19)
Closes #18
2023-06-06 21:16:13 +02:00
wkoot 88edcde8d6 Unify api serializers (#16) 2023-04-19 10:14:32 +02:00
wkoot 7f18a3ad6f Version bump NetBox 3.4.7 (#15)
Closes #14
2023-03-31 10:02:35 +02:00
wkoot 1925be271a Set up continuous integration (#13)
Closes #3
2023-03-29 22:00:48 +02:00
Hedde van der Heide a53486ca23 bump version to 1.2 2022-05-19 11:08:57 +02:00
Hedde van der Heide 4bc418a5d0 fixed filter bug versions api and set required on manufacturer for softwareproducts add 2022-05-19 11:06:34 +02:00
72 changed files with 3308 additions and 1123 deletions
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
COMPOSE_PROJECT_NAME=netbox-docker
+43
View File
@@ -0,0 +1,43 @@
---
name: Docker Image CI
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- 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
- name: Verify the Docker image
# smoke test by checking if the migrations for app netbox_slm can be displayed
run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py showmigrations netbox_slm"
- name: Run Django tests with coverage for netbox_slm
run: |
chmod go+w ci/reports
docker compose run netbox sh -c "\
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm &&\
/opt/netbox/venv/bin/coverage report --fail-under=0 &&\
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
sed -i "s/<source>\/opt\/netbox\/netbox<\/source>/<source><\/source>/" ci/reports/coverage.xml
timeout-minutes: 6
- name: Sonar scan
if: env.SONAR_TOKEN != null
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+25
View File
@@ -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"
+40
View File
@@ -0,0 +1,40 @@
---
name: Python
on:
push:
branches:
- master
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Install ci tools
run: pip install .[ci]
- name: Run ruff lint
run: ruff check netbox_slm
- name: Run ruff format
run: ruff format --check netbox_slm
+25
View File
@@ -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
+6
View File
@@ -1,3 +1,4 @@
**/netbox
**/netbox-docker
**/*.pyc
**/__pycache__
@@ -5,3 +6,8 @@
**/.vscode
**/dist
**/netbox_slm.egg-info
ci/docker-compose.override.yml
ci/reports
out/production
*.iml
build/
+123
View File
@@ -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
+133
View File
@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement by
[opening a new issue in this repository's issue tracker][open issue].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
[open issue]: https://github.com/ICTU/netbox_slm/issues/new
+34
View File
@@ -0,0 +1,34 @@
# NetBox SLM contributing guidelines
## What do I need to know to help?
If you are looking to help to with a code contribution our project uses, you can find inspiration in the [issue list](https://github.com/ICTU/netbox_slm/issues/).
Additionally, review the [README](./README.md) document and [NetBox Plugin Development](https://docs.netbox.dev/en/stable/plugins/development/) docs.
## How do I make a contribution?
Never made an open source contribution before? Wondering how contributions work in the in our project? Here's a quick rundown!
1. Find an issue that you are interested in addressing or a feature that you would like to add.
2. Fork the repository associated with the issue to your local GitHub organization. This means that you will have a copy of the repository under **your-GitHub-username/repository-name**.
3. Clone the repository to your local machine using `git clone https://github.com/github-username/repository-name.git`.
4. Create a new branch for your fix using `git checkout -b branch-name-here`.
5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add.
6. Use git `add insert-paths-of-changed-files-here` to add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index.
7. Use `git commit -m "Insert a short message of the changes made here"` to store the contents of the index with a descriptive message.
8. Push the changes to the remote repository using `git push origin branch-name-here`.
9. Submit a pull request to the upstream repository.
10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #4352".
11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it!
12. Wait for the pull request to be reviewed by a maintainer.
13. Make changes to the pull request if the reviewing maintainer recommends them.
14. Celebrate your success after your pull request is merged!
## Where can I go for help?
If you need help, you can ask questions by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new).
## What does the Code of Conduct mean for me?
Our [Code of Conduct](./CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity.
If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code.
+198 -10
View File
@@ -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
View File
@@ -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 theres 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.
+7 -3
View File
@@ -1,3 +1,7 @@
include ../LICENSE
include ../README.rst
recursive-include netbox_slm/templates *
include LICENSE
include README.md
include SECURITY.md
graft netbox_slm
prune netbox_slm/tests
global-exclude *.py[cod]
global-exclude __pycache__
-12
View File
@@ -1,12 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
twine = "3.7.1"
[requires]
python_version = "3.9"
Generated
-192
View File
@@ -1,192 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "f3cebe84c169ba6773cfcfae65b47e5076fa331e445f47a6ec4d13306942deb4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {},
"develop": {
"bleach": {
"hashes": [
"sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
"sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
],
"markers": "python_version >= '3.6'",
"version": "==4.1.0"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
],
"markers": "python_version >= '3'",
"version": "==2.0.10"
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.4"
},
"docutils": {
"hashes": [
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.18.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"importlib-metadata": {
"hashes": [
"sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
"sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
],
"markers": "python_version >= '3.7'",
"version": "==4.10.0"
},
"keyring": {
"hashes": [
"sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
"sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"
],
"markers": "python_version >= '3.7'",
"version": "==23.5.0"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pkginfo": {
"hashes": [
"sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
"sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
],
"version": "==1.8.2"
},
"pygments": {
"hashes": [
"sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4",
"sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"
],
"markers": "python_version >= '3.5'",
"version": "==2.11.1"
},
"pyparsing": {
"hashes": [
"sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
"sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.6"
},
"readme-renderer": {
"hashes": [
"sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
"sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
],
"markers": "python_version >= '3.6'",
"version": "==32.0"
},
"requests": {
"hashes": [
"sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5",
"sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.27.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"rfc3986": {
"hashes": [
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.5.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.16.0"
},
"tqdm": {
"hashes": [
"sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c",
"sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.62.3"
},
"twine": {
"hashes": [
"sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
"sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
],
"index": "pypi",
"version": "==3.7.1"
},
"urllib3": {
"hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.7"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"zipp": {
"hashes": [
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
],
"markers": "python_version >= '3.7'",
"version": "==3.7.0"
}
}
}
+31 -194
View File
@@ -1,6 +1,6 @@
<h1 align="center">NetBox SLM</h1>
# NetBox SLM
<p align="center"><i>Netbox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
<p align="center"><i>NetBox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
<div align="center">
<a href="https://pypi.org/project/netbox_slm/"><img src="https://img.shields.io/pypi/v/netbox_slm" alt="PyPi"/></a>
@@ -13,213 +13,50 @@
</div>
## Known Issues
- WARNING: This plugin is only tested with Netbox 3.2 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/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.1`` as
``configuration/plugins.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
```
FROM netboxcommunity/netbox:v3.2.X
```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.4'
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``
And finally, run NetBox with the SLM plugin: ``docker compose up``
## Releasing Guide
To draft a release;
## Get in touch
update the setup.py file to reflect the new version, then from the *src*
directory run
```
# make sure to update the version in netbox_slm/__init__.py
$ python setup.py sdist
$ twine upload dist/*
```
On Github.com create a similar tag and version. These steps could be
automated with a github workflow.
n.b. Currently the plugin is configured to use a personal pypi account,
this should be changed.
## Developer Guide (local installation)
*Follow the steps below on your local system to run netbox and the
netbox_slm plugin in developer mode*
### Setup
The goal below is to run all Netbox components in Docker and run a local
Netbox Django copy with auto-reload to develop the plugin pointing to
the Dockerized postgres and redis instances, basically ignoring the
netbox docker runtime server.
### Steps
from your projects directory clone the netbox repository
```
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
```
install the virtual environment
```
$ pipenv shell
$ pipenv install
```
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
```
DEBUG = True
SECRET_KEY = 'dummy'
DEVELOPER = True
PLUGINS = [
'netbox_slm',
]
```
The Netbox installation above will be used to run Django management
commands like runserver, makemigrations and migrate, which will be
explained in the next steps below;
from your projects directory clone the netbox-slm repository
```
$ git clone https://github.com/ICTU/netbox_slm
$ cd netbox_slm
$ ./start-netbox.sh
```
This will start Netbox locally (requires Docker) and forward the redis
and postgres ports to the localhost (make sure theres no processes
using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand:
```
$ cd netbox-docker
$ docker-compose up -d
```
or stop the stack with
```
$ docker-compose down
```
# to start fresh:
```
$ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate
```
this will require you to re-run the migrate commando's for netbox-slm, see further down below
Go back to the netbox configuration.py file and update the postgres and
redis connection strings (username, password) to the ones the netbox
docker backend is using, for example (using default user and passwords
from the netbox docker example):
```
<<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
<<collapsed>>
```
Now you can run commands from the netbox repository like this;
```
$ cd netbox/netbox
$ export PYTHONPATH=../../netbox_slm/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.
Point of contact for this repository is [Mart Visser](https://github.com/MartVisser), who can be reached by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new).
+7 -4
View File
@@ -1,13 +1,16 @@
# Security Policy
# NetBox SLM Security Policy
## Current status
Netbox SLM is a plugin for Netbox with mostly default configuration. We advise to monitor Netbox security findings and update accordingly.
NetBox SLM is a plugin for [NetBox](https://github.com/netbox-community/netbox/) with mostly default configuration.
In order to stay up to date, monitor NetBox security findings and update accordingly.
## Supported Versions
Only the latest version of Netbox SLM is currently being supported with security updates. We aim to keep the plugin compatible with the latest Netbox version, but have no incentive to patch older tags at this point.
Only the latest version of NetBox SLM is currently being supported with security updates.
The intention is to keep the plugin compatible with the most recent NetBox version(s), there is no incentive to patch older tags.
## Reporting a Vulnerability
Please report security vulnerabilities by opening a ticket. The aim is to get back to you within 24 hours with a confirmation of the issue and a brief action plan or a request for more information.
You can privately [report a vulnerability issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/security/advisories/new).
The aim is to get back to you within 24 hours, with a confirmation of the issue and a brief action plan or a request for more information.
+12
View File
@@ -0,0 +1,12 @@
StylesPath = styles
MinAlertLevel = suggestion
Vocab = Base
Packages = proselint
[*.md]
BasedOnStyles = Vale, proselint
[ci/styles/**]
BasedOnStyles =
+18
View File
@@ -0,0 +1,18 @@
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 python3 -m venv /opt/netbox/venv &&\
source /opt/netbox/venv/bin/activate
COPY ../ /build
RUN pip install -U .[build] &&\
python -m build &&\
pip install --no-index /build
FROM netboxcommunity/netbox:v4.6.3
ARG PYTHON_VERSION=3.14
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
+5
View File
@@ -0,0 +1,5 @@
FROM netboxcommunity/netbox:v4.6.3
ARG PYTHON_VERSION=3.14
COPY ../pyproject.toml /tmp/
RUN uv pip install -r /tmp/pyproject.toml --extra ci
+48
View File
@@ -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',
}
+32
View File
@@ -0,0 +1,32 @@
---
services:
netbox:
ports:
- "8001:8080"
build:
context: ..
dockerfile: ci/Dockerfile-CI
image: netbox:slm
environment:
- COVERAGE_FILE=/tmp/.coverage
healthcheck:
disable: true
volumes:
- ./config:/etc/netbox/config:z,ro
- ./reports:/ci/reports:rw
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
netbox-worker:
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"
redis:
ports:
- "6379:6379"
+89
View File
@@ -0,0 +1,89 @@
---
# based on https://github.com/netbox-community/netbox-docker/blob/5.0.1/docker-compose.yml
services:
netbox: &netbox
image: docker.io/netboxcommunity/netbox:${VERSION-v4.6.3}
depends_on:
- postgres
- redis
- redis-cache
env_file: env/netbox.env
user: "netbox:root"
healthcheck:
test: curl -f http://localhost:8080/login/ || exit 1
start_period: 90s
timeout: 3s
interval: 15s
volumes:
- ./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:
netbox:
condition: service_healthy
command:
- /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: 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:/var/lib/postgresql
# redis
redis:
image: docker.io/valkey/valkey:9.0-alpine
command:
- 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
redis-cache:
image: docker.io/valkey/valkey:9.0-alpine
command:
- 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:
driver: local
netbox-redis-cache-data:
driver: local
netbox-redis-data:
driver: local
netbox-reports-files:
driver: local
netbox-scripts-files:
driver: local
+42
View File
@@ -0,0 +1,42 @@
CORS_ORIGIN_ALLOW_ALL=True
DB_HOST=postgres
DB_NAME=netbox
DB_PASSWORD=J5brHrAXFLQSif0K
DB_USER=netbox
EMAIL_FROM=netbox@bar.com
EMAIL_PASSWORD=
EMAIL_PORT=25
EMAIL_SERVER=localhost
EMAIL_SSL_CERTFILE=
EMAIL_SSL_KEYFILE=
EMAIL_TIMEOUT=5
EMAIL_USERNAME=netbox
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
EMAIL_USE_SSL=false
EMAIL_USE_TLS=false
GRAPHQL_ENABLED=true
HOUSEKEEPING_INTERVAL=86400
MAX_PAGE_SIZE=1000
MEDIA_ROOT=/opt/netbox/netbox/media
METRICS_ENABLED=false
NAPALM_PASSWORD=
NAPALM_TIMEOUT=10
NAPALM_USERNAME=
REDIS_CACHE_DATABASE=1
REDIS_CACHE_HOST=redis-cache
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
REDIS_CACHE_SSL=false
REDIS_DATABASE=0
REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81
REDIS_SSL=false
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
SKIP_SUPERUSER=false
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin
SUPERUSER_PASSWORD=admin
WEBHOOKS_ENABLED=true
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_DB=netbox
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
POSTGRES_USER=netbox
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=H733Kdjndks81
View File
@@ -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
+31 -9
View File
@@ -1,17 +1,39 @@
from extras.plugins import PluginConfig
"""
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.9.0"
class SLMConfig(PluginConfig):
name = 'netbox_slm'
verbose_name = 'Software Lifecycle Management'
description = 'Software Lifecycle Management'
version = '1.1'
author = 'Hedde van der Heide'
author_email = 'hedde.vanderheide@ictu.nl'
base_url = 'slm'
name = "netbox_slm"
verbose_name = "Software Lifecycle Management"
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
"top_level_menu": True,
"link_cluster_installations": "right",
"link_device_installations": "right",
"link_virtualmachine_installations": "right",
}
config = SLMConfig
+7
View File
@@ -0,0 +1,7 @@
from django.apps import apps
from django.conf import settings
from django.contrib import admin
if settings.DEBUG:
for model in apps.get_app_config("netbox_slm").get_models():
admin.site.register(model)
+106 -37
View File
@@ -1,55 +1,124 @@
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,
)
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
class SoftwareLicenseSerializer(NetBoxModelSerializer):
display = SerializerMethodField()
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
class Meta:
model = SoftwareLicense
fields = (
"id",
"display",
"url",
"name",
"description",
"type",
"spdx_expression",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"support",
"license_amount",
"software_product",
"version",
"installation",
"tags",
"comments",
"custom_field_data",
"created",
"last_updated",
)
brief_fields = ("id", "display", "url", "name", "description")
def get_display(self, obj):
return f"{obj}"
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
fields = [
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
]
fields = (
"id",
"display",
"url",
"name",
"description",
"manufacturer",
"description",
"tags",
"comments",
"custom_field_data",
"created",
"last_updated",
)
brief_fields = ("id", "display", "url", "name", "description")
def get_display(self, obj):
return f"{obj.manufacturer.name} - {obj.name}"
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
class Meta:
model = SoftwareProductVersion
fields = [
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
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
fields = [
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version', 'tags',
'custom_field_data', 'created', 'last_updated',
]
fields = (
"id",
"display",
"url",
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
"comments",
"custom_field_data",
"created",
"last_updated",
)
brief_fields = ("id", "display", "url")
def get_display(self, obj):
return obj
return f"{obj}"
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
display = SerializerMethodField()
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
class Meta:
model = SoftwareProductVersion
fields = (
"id",
"display",
"url",
"name",
"description",
"release_date",
"documentation_url",
"end_of_support",
"filename",
"file_checksum",
"file_link",
"release_type",
"software_product",
"tags",
"comments",
"custom_field_data",
"created",
"last_updated",
)
brief_fields = ("id", "display", "url", "name", "description")
def get_display(self, obj):
return f"{obj}"
+4 -1
View File
@@ -4,6 +4,7 @@ from netbox_slm.api.views import (
SoftwareProductViewSet,
SoftwareProductVersionViewSet,
SoftwareProductInstallationViewSet,
SoftwareLicenseViewSet,
)
router = NetBoxRouter()
@@ -12,4 +13,6 @@ router.APIRootView = NetboxSLMRootView
router.register("softwareproducts", SoftwareProductViewSet)
router.register("softwareproductversions", SoftwareProductVersionViewSet)
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
urlpatterns = router.urls
router.register("softwarelicenses", SoftwareLicenseViewSet)
urlpatterns = router.urls
+22 -11
View File
@@ -1,39 +1,50 @@
from netbox.api.viewsets import NetBoxModelViewSet
from rest_framework.routers import APIRootView
from netbox.api.viewsets import NetBoxModelViewSet
from netbox_slm.api.serializers import (
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
SoftwareProductSerializer,
SoftwareProductVersionSerializer,
SoftwareProductInstallationSerializer,
SoftwareLicenseSerializer,
)
from netbox_slm.filtersets import (
SoftwareProductFilterSet, SoftwareProductVersionFilterSet, SoftwareProductInstallationFilterSet,
SoftwareProductFilterSet,
SoftwareProductVersionFilterSet,
SoftwareProductInstallationFilterSet,
SoftwareLicenseFilterSet,
)
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
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().order_by("id")
serializer_class = SoftwareLicenseSerializer
filterset_class = SoftwareLicenseFilterSet
+102 -9
View File
@@ -1,11 +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 *
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()
@@ -14,13 +29,34 @@ class SoftwareProductFilterSet(NetBoxModelFilterSet):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | \
Q(manufacturer__name__icontains=value)
qs_filter = (
Q(name__icontains=value)
| Q(description__icontains=value)
| Q(manufacturer__name__icontains=value)
| Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
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 = tuple()
@@ -29,14 +65,31 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | \
Q(software_product__name__icontains=value) | \
Q(software_product__manufacturer__name__icontains=value)
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)
)
return queryset.filter(qs_filter)
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 = tuple()
@@ -45,7 +98,47 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(software_product__name__icontains=value) | \
Q(software_product__manufacturer__name__icontains=value) | \
Q(version__name__icontains=value)
qs_filter = (
Q(software_product__name__icontains=value)
| Q(software_product__manufacturer__name__icontains=value)
| Q(version__name__icontains=value)
| Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
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 = ("support",)
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
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)
| Q(installation__virtualmachine__name__icontains=value)
| Q(installation__cluster__name__icontains=value)
| Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
-174
View File
@@ -1,174 +0,0 @@
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from dcim.models import Manufacturer, Device
from netbox.forms import (
NetBoxModelForm,
NetBoxModelCSVForm,
NetBoxModelBulkEditForm,
NetBoxModelFilterSetForm,
)
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import (
DynamicModelChoiceField, APISelect, TagFilterField, ChoiceField
)
from virtualization.models import VirtualMachine
class SoftwareProductForm(NetBoxModelForm):
"""Form for creating a new SoftwareProduct object."""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
)
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer", "description", "tags")
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct
fieldsets = (
(None, ('q', 'tag')),
)
tag = TagFilterField(model)
class SoftwareProductCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer",)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
model = SoftwareProduct
nullable_fields = []
class SoftwareProductVersionForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductVersion object."""
name = forms.CharField(label=_("Version"))
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product", "tags")
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductVersion
fieldsets = (
(None, ('q', 'tag')),
)
tag = TagFilterField(model)
class SoftwareProductVersionCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProductVersion
fields = ("name",)
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
model = SoftwareProductVersion
nullable_fields = []
class SoftwareProductInstallationForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={
'software_product': '$software_product',
}
)
class Meta:
model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version", "tags")
def clean_version(self):
version = self.cleaned_data['version']
software_product = self.cleaned_data['software_product']
if version not in software_product.softwareproductversion_set.all():
raise forms.ValidationError(_(f"Version `{version}` doesn't exist on {software_product}, make sure you've "
f"selected a compatible version or first select the software product."))
return version
def clean(self):
if not any([self.cleaned_data['device'], self.cleaned_data['virtualmachine']]):
raise forms.ValidationError(_("Installation requires atleast one virtualmachine or device destination."))
return super(SoftwareProductInstallationForm, self).clean()
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductInstallation
fieldsets = (
(None, ('q', 'tag',)),
)
tag = TagFilterField(model)
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProductInstallation
fields = tuple()
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
model = SoftwareProductInstallation
fieldsets = (
(None, ('software_product', 'version',)),
)
+24
View File
@@ -0,0 +1,24 @@
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,
)
+199
View File
@@ -0,0 +1,199 @@
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField
from django.urls import reverse_lazy
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):
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,
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"),
)
class Meta:
model = SoftwareLicense
fields = (
"name",
"description",
"software_product",
"type",
"spdx_expression",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"support",
"license_amount",
"version",
"installation",
"tags",
"comments",
)
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
model = SoftwareLicense
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"),
)
+75
View File
@@ -0,0 +1,75 @@
from django.forms import CharField
from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import SoftwareProduct
from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
class SoftwareProductForm(NetBoxModelForm):
comments = CommentField()
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
)
class Meta:
model = SoftwareProduct
fields = (
"name",
"description",
"manufacturer",
"tags",
"comments",
)
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct
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,
)
@@ -0,0 +1,138 @@
from django.forms import ValidationError
from django.urls import reverse_lazy
from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
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 Cluster, VirtualMachine
class SoftwareProductInstallationForm(NetBoxModelForm):
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=True,
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=dict(software_product="$software_product"),
)
class Meta:
model = SoftwareProductInstallation
fields = (
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
"comments",
)
def clean_version(self):
version = self.cleaned_data["version"]
software_product = self.cleaned_data["software_product"]
if version not in software_product.softwareproductversion_set.all():
raise ValidationError(
f"Version '{version}' doesn't exist on {software_product}, make sure you've "
f"selected a compatible version or first select the software product."
)
return version
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductInstallation
fieldsets = (
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"),
)
@@ -0,0 +1,126 @@
from django.forms import DateField, CharField, MultipleChoiceField
from django.urls import reverse_lazy
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):
comments = CommentField()
release_date = DateField(required=False, widget=DatePicker())
end_of_support = DateField(required=False, widget=DatePicker())
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")}),
)
class Meta:
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",
"filename",
"file_checksum",
"file_link",
"release_type",
"software_product",
),
)
nullable_fields = ("release_date", "documentation_url", "end_of_support", "filename", "file_checksum", "file_link")
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",
)
@@ -0,0 +1,54 @@
# Generated by Django 4.1.8 on 2023-06-27 08:10
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('extras', '0084_staging'),
('netbox_slm', '0004_auto_20220415_0705'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.AlterField(
model_name='softwareproductversion',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.CreateModel(
name='SoftwareLicense',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('name', models.CharField(max_length=128)),
('description', models.CharField(blank=True, max_length=255, null=True)),
('type', models.CharField(max_length=128)),
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
('start_date', models.DateField(blank=True, null=True)),
('expiration_date', models.DateField(blank=True, null=True)),
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
],
options={
'abstract': False,
},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.2.5 on 2023-09-22 00:34
from django.db import migrations
import netbox_slm.models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0005_add_software_license'),
]
operations = [
migrations.AddField(
model_name='softwarelicense',
name='stored_location_url',
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.2.5 on 2023-10-04 10:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0036_virtualmachine_config_template'),
('netbox_slm', '0006_softwarelicense_stored_location_url'),
]
operations = [
migrations.AddField(
model_name='softwareproductinstallation',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
),
migrations.AlterField(
model_name='softwarelicense',
name='installation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
),
migrations.AddConstraint(
model_name='softwareproductinstallation',
constraint=models.CheckConstraint(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,79 @@
# Generated by Django 4.2.9 on 2024-02-07 21:01
from django.db import migrations, models
import netbox_slm.models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0007_softwareproductinstallation_cluster'),
]
operations = [
migrations.AddField(
model_name='softwarelicense',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='softwarelicense',
name='license_amount',
field=models.PositiveIntegerField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='softwarelicense',
name='support',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='softwareproduct',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='softwareproductinstallation',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='documentation_url',
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='end_of_support',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='file_checksum',
field=models.CharField(blank=True, max_length=128, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='file_link',
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='filename',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='release_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='softwareproductversion',
name='release_type',
field=models.CharField(default='S', max_length=3),
),
]
@@ -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],
),
),
]
+129 -42
View File
@@ -1,20 +1,31 @@
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):
"""
NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField
Overriding default_validators is needed, as they are always added
"""
default_validators = [EnhancedURLValidator()]
class SoftwareProduct(NetBoxModel):
name = models.CharField(max_length=128)
comments = models.TextField(blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
null=True, blank=True
)
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
objects = RestrictedQuerySet.as_manager()
@@ -26,18 +37,41 @@ 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):
ALPHA = "A", "Alpha"
BETA = "B", "Beta"
RELEASE_CANDIDATE = "RC", "Release candidate"
STABLE = "S", "Stable release"
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)
filename = models.CharField(max_length=64, null=True, blank=True)
file_checksum = models.CharField(max_length=128, null=True, blank=True)
file_link = LaxURLField(max_length=1024, null=True, blank=True)
release_type = models.CharField(
max_length=3,
choices=SoftwareReleaseTypes.choices,
default=SoftwareReleaseTypes.STABLE,
)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
to="netbox_slm.SoftwareProduct",
on_delete=models.PROTECT,
)
name = models.CharField(max_length=64)
objects = RestrictedQuerySet.as_manager()
@@ -49,44 +83,97 @@ 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):
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.PROTECT,
null=True,
blank=True
)
comments = models.TextField(blank=True)
device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
virtualmachine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.PROTECT,
null=True,
blank=True
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT,
)
version = models.ForeignKey(
to='netbox_slm.SoftwareProductVersion',
on_delete=models.PROTECT,
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return f"{self.pk} ({self.platform})"
class Meta:
constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_platform",
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)
),
violation_error_message="Installation requires exactly one platform destination.",
)
]
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
@property
def platform(self):
return self.device or self.virtualmachine or self.cluster
def render_type(self):
if self.device:
return "device"
if self.virtualmachine:
return "virtualmachine"
return "cluster"
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)
expiration_date = models.DateField(null=True, blank=True)
support = models.BooleanField(default=None, null=True, blank=True)
license_amount = models.PositiveIntegerField(default=None, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
installation = models.ForeignKey(
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return f"{self.pk}"
return self.name
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
def get_platform(self):
return self.device or self.virtualmachine
def render_type(self):
return f"{'device' if self.device else 'virtualmachine'}"
@property
def stored_location_txt(self):
if self.stored_location_url and not self.stored_location:
return "Link"
return self.stored_location
+50 -25
View File
@@ -1,66 +1,91 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
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',
link="plugins:netbox_slm:softwareproduct_list",
link_text="Software Products",
permissions=["netbox_slm.add_softwareproduct"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add",
"Add",
"mdi mdi-plus-thick",
ButtonColorChoices.GREEN,
permissions=["netbox_slm.add_softwareproduct"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_import",
"plugins:netbox_slm:softwareproduct_bulk_import",
"Import",
"mdi mdi-upload",
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproduct"],
permissions=["netbox_slm.import_softwareproduct"],
),
)
),
),
PluginMenuItem(
link='plugins:netbox_slm:softwareproductversion_list',
link_text='Versions',
link="plugins:netbox_slm:softwareproductversion_list",
link_text="Versions",
permissions=["netbox_slm.add_softwareproductversion"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add",
"Add",
"mdi mdi-plus-thick",
ButtonColorChoices.GREEN,
permissions=["netbox_slm.add_softwareproductversion"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_import",
"plugins:netbox_slm:softwareproductversion_bulk_import",
"Import",
"mdi mdi-upload",
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductversion"],
permissions=["netbox_slm.import_softwareproductversion"],
),
)
),
),
PluginMenuItem(
link='plugins:netbox_slm:softwareproductinstallation_list',
link_text='Installations',
link="plugins:netbox_slm:softwareproductinstallation_list",
link_text="Installations",
permissions=["netbox_slm.add_softwareproductinstallation"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add",
"Add",
"mdi mdi-plus-thick",
ButtonColorChoices.GREEN,
permissions=["netbox_slm.add_softwareproductinstallation"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_import",
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
"Import",
"mdi mdi-upload",
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductinstallation"],
permissions=["netbox_slm.import_softwareproductinstallation"],
),
)
),
),
PluginMenuItem(
link="plugins:netbox_slm:softwarelicense_list",
link_text="Licenses",
permissions=["netbox_slm.add_softwarelicense"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add",
"Add",
"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
+92 -78
View File
@@ -1,9 +1,8 @@
import django_tables2 as tables
from django.db.models import Count, F, Value
from django.db.models import Count
from django_tables2.utils import Accessor
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from netbox.tables import NetBoxTable, ChoiceFieldColumn, ToggleColumn, columns
from netbox.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
class SoftwareProductTable(NetBoxTable):
@@ -11,23 +10,18 @@ class SoftwareProductTable(NetBoxTable):
pk = ToggleColumn()
name = tables.LinkColumn()
manufacturer = tables.Column(
accessor=Accessor('manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count")
tags = columns.TagColumn(
url_name="plugins:netbox_slm:softwareproduct_list",
)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProduct
fields = (
"pk",
"name",
"manufacturer",
"description",
"manufacturer",
"installations",
"tags",
)
@@ -35,21 +29,14 @@ 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(("-" if is_descending else "") + "count")
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
@@ -57,50 +44,41 @@ class SoftwareProductVersionTable(NetBoxTable):
"""Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Version')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
manufacturer = tables.Column(
accessor=Accessor('software_product__manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
name = tables.LinkColumn()
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")
tags = columns.TagColumn(
url_name="plugins:netbox_slm:softwareproductversion_list",
)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProductVersion
fields = (
"pk",
"name",
"description",
"software_product",
"manufacturer",
"release_date",
"end_of_support",
"release_type",
"installations",
"tags",
)
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(("-" if is_descending else "") + "count")
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
@@ -108,40 +86,21 @@ class SoftwareProductInstallationTable(NetBoxTable):
"""Table for displaying SoftwareProductInstallation objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
device = tables.Column(
accessor=Accessor('device'),
linkify=True
)
virtualmachine = tables.Column(
accessor=Accessor('virtualmachine'),
linkify=True
)
platform = tables.Column(
accessor='get_platform',
linkify=True
)
type = tables.Column(accessor='render_type')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
version = tables.Column(
accessor=Accessor('version'),
linkify=True
)
platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type")
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",
)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProductInstallation
fields = (
"pk",
"name",
"platform",
"type",
"manufacturer",
"software_product",
"version",
"tags",
@@ -150,18 +109,73 @@ class SoftwareProductInstallationTable(NetBoxTable):
"pk",
"platform",
"type",
"manufacturer",
"software_product",
"version",
"tags",
)
def order_platform(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name"))
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name"))
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True
def order_type(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine"))
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster"))
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
def render_software_product(self, value, **kwargs):
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
class SoftwareLicenseTable(NetBoxTable):
"""Table for displaying SoftwareLicense objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
type = tables.Column()
spdx_expression = tables.Column(verbose_name="SPDX expression")
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
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)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
class Meta(NetBoxTable.Meta):
model = SoftwareLicense
fields = (
"pk",
"name",
"description",
"type",
"spdx_expression",
"stored_location",
"start_date",
"expiration_date",
"manufacturer",
"software_product",
"version",
"installation",
"support",
"license_amount",
"tags",
)
default_columns = (
"pk",
"name",
"type",
"manufacturer",
"software_product",
"installation",
"expiration_date",
"tags",
)
def render_installation(self, **kwargs):
return f"{kwargs['record'].installation.platform}"
+36
View File
@@ -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>
@@ -0,0 +1,80 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software License
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.type }}</td>
</tr>
<tr>
<th scope="row">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 %}
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
{% else %}
<td>{{ object.stored_location_txt }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">Start date</th>
<td>{{ object.start_date }}</td>
</tr>
<tr>
<th scope="row">Expiration date</th>
<td>{{ object.expiration_date }}</td>
</tr>
<tr>
<th scope="row">Support</th>
<td>{{ object.support }}</td>
</tr>
<tr>
<th scope="row">License amount</th>
<td>{{ object.license_amount }}</td>
</tr>
</table>
</div>
{% 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 %}
</div>
</div>
{% endblock %}
@@ -11,36 +11,66 @@
<h5 class="card-header">
Software Product
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproduct_versions.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
</a>
{% empty %}
n/a
{% endfor %}
</td>
</tr>
</table>
</div>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>{{ object.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproductversion_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
{{ version }}
</a>
{% 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 %}
@@ -11,39 +11,54 @@
<h5 class="card-header">
Software Product Installation
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% if object.device %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Virtualmachine</th>
<td>{{ object.virtualmachine }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Software Product</th>
<td>{{ object.software_product }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<td>{{ object.version }}</td>
</tr>
</table>
</div>
<table class="table table-hover attr-table">
{% if object.device %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device|linkify }}</td>
</tr>
{% elif object.virtualmachine %}
<tr>
<th scope="row">Virtual Machine</th>
<td>{{ object.virtualmachine|linkify }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Cluster</th>
<td>{{ object.cluster|linkify }}</td>
</tr>
{% endif %}
<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">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 %}
@@ -11,30 +11,90 @@
<h5 class="card-header">
Software Product Version
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
{{ object.get_installation_count }}
</td>
</tr>
</table>
</div>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>{{ object.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>
</tr>
<tr>
<th scope="row">Documentation url</th>
{% if object.documentation_url %}
<td><a href="{{ object.documentation_url }}">{{ object.documentation_url }}</a></td>
{% else %}
<td>{{ object.documentation_url }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">End of support</th>
<td>{{ object.end_of_support }}</td>
</tr>
<tr>
<th scope="row">Filename</th>
{% if object.file_link %}
<td><a href="{{ object.file_link }}">{{ object.filename }}</a></td>
{% else %}
<td>{{ object.filename }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">File checksum</th>
<td>{{ object.file_checksum }}</td>
</tr>
<tr>
<th scope="row">Release type</th>
<td>{{ object.get_release_type_display }}</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 %}
View File
+59
View File
@@ -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()
+51
View File
@@ -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")
+58
View File
@@ -0,0 +1,58 @@
from .base import SlmBaseTestCase
class ModelTestCase(SlmBaseTestCase):
"""Test basic model functionality and custom overrides"""
def test_model_name(self):
self.assertEqual(self.p_name, str(self.software_product))
self.assertEqual(self.v_name, str(self.software_product_version))
self.assertTrue(str(self.software_product_installation)[0].isdigit()) # starts with PK
self.assertEqual(self.l_name, str(self.software_license))
def test_absolute_url(self):
self.assertEqual(
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):
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())
self.assertEqual(self.vm, self.software_product_installation.platform)
self.software_product_installation.virtualmachine = None
self.software_product_installation.device = self.device
self.software_product_installation.save()
self.assertEqual("device", self.software_product_installation.render_type())
self.assertEqual(self.device, self.software_product_installation.platform)
self.software_product_installation.device = None
self.software_product_installation.cluster = self.cluster
self.software_product_installation.save()
self.assertEqual("cluster", self.software_product_installation.render_type())
self.assertEqual(self.cluster, self.software_product_installation.platform)
def test_software_license_stored_location_txt(self):
self.assertEqual("Link", self.software_license.stored_location_txt)
self.software_license.stored_location = "GitHub"
self.software_license.save()
self.assertEqual("GitHub", self.software_license.stored_location_txt)
+11 -64
View File
@@ -1,68 +1,15 @@
from django.urls import path
from netbox.views.generic import ObjectChangeLogView
from django.urls import path, include
from netbox_slm import views
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from netbox_slm import views # noqa F401
from utilities.urls import get_model_urls
urlpatterns = [
# Software Products
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(),
name="softwareproduct_bulk_delete"),
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(),
name="softwareproduct_delete"),
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
path(
"software-products/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproduct_changelog",
kwargs={"model": SoftwareProduct},
),
# Software Product Versions
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(),
name="softwareproductversion_import"),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(),
name="softwareproductversion_bulk_delete"),
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(),
name="softwareproductversion_delete"),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path(
"versions/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductversion_changelog",
kwargs={"model": SoftwareProductVersion},
),
# Software Product Versions
path("installations/", views.SoftwareProductInstallationListView.as_view(),
name='softwareproductinstallation_list'),
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(),
name='softwareproductinstallation_add'),
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(),
name="softwareproductinstallation_import"),
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(),
name="softwareproductinstallation_bulk_edit"),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(),
name="softwareproductinstallation_bulk_delete"),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(),
name="softwareproductinstallation"),
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(),
name="softwareproductinstallation_delete"),
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(),
name="softwareproductinstallation_edit"),
path(
"installations/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductinstallation_changelog",
kwargs={"model": SoftwareProductInstallation},
),
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"))),
]
-154
View File
@@ -1,154 +0,0 @@
from netbox.views import generic
from netbox_slm import filtersets
from netbox_slm import forms
from netbox_slm import tables
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
)
class SoftwareProductListView(generic.ObjectListView):
"""View for listing all existing SoftwareProducts."""
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
class SoftwareProductView(generic.ObjectView):
"""Display SoftwareProduct details"""
queryset = SoftwareProduct.objects.all()
def get_extra_context(self, request, instance):
versions = instance.softwareproductversion_set.all()
return {"versions": versions}
class SoftwareProductEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProduct instance."""
queryset = SoftwareProduct.objects.all()
form = forms.SoftwareProductForm
class SoftwareProductDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProduct instance"""
queryset = SoftwareProduct.objects.all()
class SoftwareProductBulkImportView(generic.BulkImportView):
queryset = SoftwareProduct.objects.all()
model_form = forms.SoftwareProductCSVForm
table = tables.SoftwareProductTable
class SoftwareProductBulkEditView(generic.BulkEditView):
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
form = forms.SoftwareProductBulkEditForm
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProduct.objects.all()
table = tables.SoftwareProductTable
class SoftwareProductVersionListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductVersions."""
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
filterset_form = forms.SoftwareProductVersionFilterForm
table = tables.SoftwareProductVersionTable
class SoftwareProductVersionView(generic.ObjectView):
"""Display SoftwareProductVersion details"""
queryset = SoftwareProductVersion.objects.all()
def get_extra_context(self, request, instance):
installation_count = instance.get_installation_count()
return {"installations": installation_count}
class SoftwareProductVersionEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductVersion instance."""
queryset = SoftwareProductVersion.objects.all()
form = forms.SoftwareProductVersionForm
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProductVersion instance"""
queryset = SoftwareProductVersion.objects.all()
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
queryset = SoftwareProductVersion.objects.all()
model_form = forms.SoftwareProductVersionCSVForm
table = tables.SoftwareProductVersionTable
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
table = tables.SoftwareProductVersionTable
form = forms.SoftwareProductVersionBulkEditForm
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductVersion.objects.all()
table = tables.SoftwareProductVersionTable
class SoftwareProductInstallationListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductInstallations."""
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
filterset_form = forms.SoftwareProductInstallationFilterForm
table = tables.SoftwareProductInstallationTable
class SoftwareProductInstallationView(generic.ObjectView):
"""Display SoftwareProductInstallation details"""
queryset = SoftwareProductInstallation.objects.all()
class SoftwareProductInstallationEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductInstallation instance."""
queryset = SoftwareProductInstallation.objects.all()
form = forms.SoftwareProductInstallationForm
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProductInstallation instance"""
queryset = SoftwareProductInstallation.objects.all()
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
queryset = SoftwareProductInstallation.objects.all()
model_form = forms.SoftwareProductInstallationCSVForm
table = tables.SoftwareProductInstallationTable
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
table = tables.SoftwareProductInstallationTable
form = forms.SoftwareProductInstallationBulkEditForm
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductInstallation.objects.all()
table = tables.SoftwareProductInstallationTable
+32
View File
@@ -0,0 +1,32 @@
from .software_license import (
SoftwareLicenseListView,
SoftwareLicenseView,
SoftwareLicenseEditView,
SoftwareLicenseDeleteView,
SoftwareLicenseBulkDeleteView,
SoftwareLicenseBulkImportView,
)
from .software_product import (
SoftwareProductListView,
SoftwareProductView,
SoftwareProductEditView,
SoftwareProductDeleteView,
SoftwareProductBulkDeleteView,
SoftwareProductBulkImportView,
)
from .software_product_installation import (
SoftwareProductInstallationListView,
SoftwareProductInstallationView,
SoftwareProductInstallationEditView,
SoftwareProductInstallationDeleteView,
SoftwareProductInstallationBulkDeleteView,
SoftwareProductInstallationBulkImportView,
)
from .software_product_version import (
SoftwareProductVersionListView,
SoftwareProductVersionView,
SoftwareProductVersionEditView,
SoftwareProductVersionDeleteView,
SoftwareProductVersionBulkDeleteView,
SoftwareProductVersionBulkImportView,
)
+50
View File
@@ -0,0 +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):
queryset = SoftwareLicense.objects.all()
filterset = filtersets.SoftwareLicenseFilterSet
filterset_form = forms.SoftwareLicenseFilterForm
table = tables.SoftwareLicenseTable
@register_model_view(SoftwareLicense)
class SoftwareLicenseView(generic.ObjectView):
queryset = SoftwareLicense.objects.all()
@register_model_view(SoftwareLicense, "add", detail=False)
@register_model_view(SoftwareLicense, "edit")
class SoftwareLicenseEditView(generic.ObjectEditView):
queryset = SoftwareLicense.objects.all()
form = forms.SoftwareLicenseForm
@register_model_view(SoftwareLicense, "delete")
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
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
+54
View File
@@ -0,0 +1,54 @@
from netbox.views import generic
from netbox_slm import filtersets, forms, tables
from netbox_slm.models import SoftwareProduct
from utilities.views import register_model_view
@register_model_view(SoftwareProduct, "list", path="", detail=False)
class SoftwareProductListView(generic.ObjectListView):
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
@register_model_view(SoftwareProduct)
class SoftwareProductView(generic.ObjectView):
queryset = SoftwareProduct.objects.all()
def get_extra_context(self, request, instance):
versions = instance.softwareproductversion_set.all()
return {"versions": versions}
@register_model_view(SoftwareProduct, "add", detail=False)
@register_model_view(SoftwareProduct, "edit")
class SoftwareProductEditView(generic.ObjectEditView):
queryset = SoftwareProduct.objects.all()
form = forms.SoftwareProductForm
@register_model_view(SoftwareProduct, "delete")
class SoftwareProductDeleteView(generic.ObjectDeleteView):
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
@@ -0,0 +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):
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
filterset_form = forms.SoftwareProductInstallationFilterForm
table = tables.SoftwareProductInstallationTable
@register_model_view(SoftwareProductInstallation)
class SoftwareProductInstallationView(generic.ObjectView):
queryset = SoftwareProductInstallation.objects.all()
@register_model_view(SoftwareProductInstallation, "add", detail=False)
@register_model_view(SoftwareProductInstallation, "edit")
class SoftwareProductInstallationEditView(generic.ObjectEditView):
queryset = SoftwareProductInstallation.objects.all()
form = forms.SoftwareProductInstallationForm
@register_model_view(SoftwareProductInstallation, "delete")
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
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
@@ -0,0 +1,54 @@
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):
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
filterset_form = forms.SoftwareProductVersionFilterForm
table = tables.SoftwareProductVersionTable
@register_model_view(SoftwareProductVersion)
class SoftwareProductVersionView(generic.ObjectView):
queryset = SoftwareProductVersion.objects.all()
def get_extra_context(self, request, instance):
installation_count = instance.get_installation_count()
return {"installations": installation_count}
@register_model_view(SoftwareProductVersion, "add", detail=False)
@register_model_view(SoftwareProductVersion, "edit")
class SoftwareProductVersionEditView(generic.ObjectEditView):
queryset = SoftwareProductVersion.objects.all()
form = forms.SoftwareProductVersionForm
@register_model_view(SoftwareProductVersion, "delete")
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
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
+84
View File
@@ -0,0 +1,84 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "netbox-slm"
dynamic = ["version"]
description = "Software Lifecycle Management Netbox Plugin"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
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 :: 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 = [
"license-expression == 30.4.4",
]
[project.optional-dependencies]
build = [
"build == 1.5.0",
"setuptools == 82.0.1",
"twine == 6.2.0",
]
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__"}
[tool.setuptools.packages.find]
include = ["netbox_slm"]
[tool.ruff]
line-length = 120
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
"F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files
]
"netbox_slm/migrations/*.py" = [
"E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files
]
[tool.ruff.format]
exclude = ["netbox_slm/migrations/*.py"]
-28
View File
@@ -1,28 +0,0 @@
[metadata]
name = netbox-slm
version = 1.1
description = Software Lifecycle Management Netbox Plugin.
# long_description = file: README.rst
# long_description_content_type='text/x-rst'
author = Hedde van der Heide
author_email = hedde.vanderheide@ictu.nl
license = Apache 2.0
classifiers =
Environment :: Web Environment
Framework :: Django
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options]
include_package_data = true
packages = find:
python_requires = >=3.7
-3
View File
@@ -1,3 +0,0 @@
from setuptools import setup, Extension
setup()
+14
View File
@@ -0,0 +1,14 @@
# Project metadata
sonar.organization=ictu
sonar.projectKey=ICTU_netbox_slm
sonar.projectName=netbox-slm
sonar.projectVersion=1.9.0
# Path is relative to the sonar-project.properties file
sonar.sources=netbox_slm
sonar.exclusions=netbox_slm/tests/**
sonar.tests=netbox_slm/tests
sonar.python.version=3.11, 3.12, 3.13, 3.14
# Python test reports
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
+1 -1
View File
@@ -1,7 +1,7 @@
git clone -b release https://github.com/netbox-community/netbox-docker.git
cd netbox-docker
tee docker-compose.override.yml <<EOF
version: '3.4'
version: '3.7'
services:
netbox:
ports: