Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d773a407a2 | ||
|
|
175a01b257 | ||
|
|
334a76e640 | ||
|
|
ef45789b30 | ||
|
|
4bd02ab6f9 | ||
|
|
410b8f6617 | ||
|
|
306d853a16 | ||
|
|
193f04a208 | ||
|
|
c3ed952b5f | ||
|
|
87fdeb1c24 | ||
|
|
a0a98bb407 | ||
|
|
3539539c4e | ||
|
|
72646a6287 | ||
|
|
86215f43a6 | ||
|
|
230b3b5115 | ||
|
|
1411e3a5cb | ||
|
|
d082af4a9b | ||
|
|
2501e41b7c | ||
|
|
aa41f8a402 | ||
|
|
6c512e0e9b | ||
|
|
5b96496b46 | ||
|
|
88edcde8d6 | ||
|
|
7f18a3ad6f | ||
|
|
1925be271a | ||
|
|
a53486ca23 | ||
|
|
4bc418a5d0 | ||
|
|
e952111a53 | ||
|
|
e73cb5fa1a | ||
|
|
56552bdd02 | ||
|
|
f914e78812 | ||
|
|
c9a7b2315e | ||
|
|
a8c9fc5fbe | ||
|
|
a1f6e3dc92 | ||
|
|
4f8053c0f6 | ||
|
|
3b3a6d63d4 | ||
|
|
ce283dea52 | ||
|
|
f3579cce63 | ||
|
|
3bb711ea84 | ||
|
|
9d76bb421d | ||
|
|
7a24221444 | ||
|
|
ac198ef7cf | ||
|
|
6d05b1b6ca | ||
|
|
049fb045c0 | ||
|
|
9e81b94d31 | ||
|
|
18fed2c029 | ||
|
|
a02acbc561 | ||
|
|
74d648b94d | ||
|
|
e9422d70e3 | ||
|
|
15b61fd7f9 | ||
|
|
c264536850 | ||
|
|
2dbcaa4877 | ||
|
|
6884bb3b94 | ||
|
|
bf47320864 | ||
|
|
f7bb83c913 | ||
|
|
0519796af6 | ||
|
|
19fc473286 | ||
|
|
f4188b957f | ||
|
|
51736942e7 | ||
|
|
bf63046edb | ||
|
|
72274e59b5 | ||
|
|
ae06bea12d | ||
|
|
6937a0ecca | ||
|
|
e7eb5b46e4 | ||
|
|
154c0de9f4 | ||
|
|
c84aa3650b | ||
|
|
fb29bc4258 | ||
|
|
7e59a5644b | ||
|
|
f1dbedcfba | ||
|
|
bbb00d667d | ||
|
|
743759f0e6 | ||
|
|
3ed18c5767 | ||
|
|
e926f01141 | ||
|
|
bc0b77f962 | ||
|
|
cec3d50313 | ||
|
|
d70f8afc41 | ||
|
|
0122275ee1 | ||
|
|
267cb8db63 | ||
|
|
adea4437c9 |
@@ -0,0 +1,3 @@
|
||||
COMPOSE_PATH_SEPARATOR=:
|
||||
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME=netbox-docker
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Markdown lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
vale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: errata-ai/vale-action@v2.1.1
|
||||
with:
|
||||
fail_on_error: true
|
||||
filter_mode: nofilter
|
||||
vale_flags: "--glob=*.md --config=ci/.vale.ini"
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- python-version: 3.11
|
||||
- python-version: 3.12
|
||||
- python-version: 3.13
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
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
|
||||
@@ -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/
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.8.1](https://github.com/ICTU/netbox_slm/releases/tag/1.8.1) - 2025-04-11
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix 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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# Maintenance related tasks
|
||||
|
||||
|
||||
## Version upgrade workflow
|
||||
|
||||
1. Update version spec in `netbox_slm/__init__.py` and `sonar-project.properties`
|
||||
1. Check for any runtime errors and warnings in the `netbox-*` container logs
|
||||
1. Create new version tag on GitHub, following semantic versioning as: `MAJOR.MINOR.PATCH`
|
||||
1. Update the `CHANGELOG.md` with new version information and move `[Unreleased]` items to new version section
|
||||
1. Build the package: `python -m build`
|
||||
1. Upload the distributions to PyPI: `twine upload --skip-existing dist/*`
|
||||
|
||||
|
||||
## Developer Guide (local installation)
|
||||
|
||||
*Follow the steps below on your local system to run NetBox and the
|
||||
`netbox_slm` plugin in developer mode*
|
||||
|
||||
### Setup
|
||||
|
||||
The goal below is to run all NetBox components in Docker and run a local
|
||||
NetBox Django copy with auto-reload to develop the plugin pointing to
|
||||
the PostgreSQL and Redis container instances, basically ignoring the
|
||||
NetBox docker runtime server.
|
||||
|
||||
### Steps
|
||||
|
||||
from your projects directory clone the NetBox repository
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
```
|
||||
|
||||
install the virtual environment
|
||||
|
||||
```shell
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
```
|
||||
|
||||
create and edit `netbox/configuration.py` (based on the template file) add these lines at the end of the file;
|
||||
|
||||
```python
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
```
|
||||
|
||||
The NetBox installation above will be used to run Django management
|
||||
commands like `runserver`, `makemigrations` and `migrate`, which will be
|
||||
explained in the next steps below;
|
||||
|
||||
from your projects directory clone the `netbox_slm` repository
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
```
|
||||
|
||||
This will start NetBox locally (requires Docker) and forward the Redis
|
||||
and PostgreSQL ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the Dockerfile(s) accordingly)
|
||||
|
||||
Note, you can also start and stop NetBox by hand:
|
||||
|
||||
```shell
|
||||
$ cd netbox-docker
|
||||
$ docker compose up -d
|
||||
```
|
||||
|
||||
or stop the stack with
|
||||
|
||||
```shell
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
#### to start fresh:
|
||||
|
||||
```shell
|
||||
$ docker compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker compose up -d --force-recreate
|
||||
```
|
||||
|
||||
this will require you to re-run the migrate commando's for `netbox_slm`, see further down below
|
||||
|
||||
Go back to the NetBox `configuration.py` file and update the PostgreSQL and
|
||||
Redis connection strings (username, password) to the ones the NetBox
|
||||
Docker backend is using, for example (using default user and passwords
|
||||
from the NetBox Docker example):
|
||||
|
||||
```python
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run commands from the NetBox repository like this;
|
||||
|
||||
```shell
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox_slm/netbox_slm/ # or with the pipenv activated run `python3 setup.py develop` from the netbox_slm directory
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
```
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browser to see the auto reloading version of the NetBox UI.
|
||||
Port 8000 is taken by the Docker ran variant.
|
||||
@@ -0,0 +1,7 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include SECURITY.md
|
||||
graft netbox_slm
|
||||
prune netbox_slm/tests
|
||||
global-exclude *.py[cod]
|
||||
global-exclude __pycache__
|
||||
@@ -1,11 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
@@ -1,153 +1,62 @@
|
||||
## Software Lifecycle Management
|
||||
# NetBox SLM
|
||||
|
||||
### _Netbox Plugin for lifecycle management of software components, including versions and installations_
|
||||
<p align="center"><i>NetBox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
<a href="https://pypi.org/project/netbox_slm/"><img src="https://img.shields.io/pypi/v/netbox_slm" alt="PyPi"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/stargazers"><img src="https://img.shields.io/github/stars/ICTU/netbox_slm" alt="Stars Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/network/members"><img src="https://img.shields.io/github/forks/ICTU/netbox_slm" alt="Forks Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/pulls"><img src="https://img.shields.io/github/issues-pr/ICTU/netbox_slm" alt="Pull Requests Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/issues"><img src="https://img.shields.io/github/issues/ICTU/netbox_slm" alt="Issues Badge"/></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/graphs/contributors"><img alt="GitHub contributors" src="https://img.shields.io/github/contributors/ICTU/netbox_slm?color=2b9348"></a>
|
||||
<a href="https://github.com/ICTU/netbox_slm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ICTU/netbox_slm?color=2b9348" alt="License Badge"/></a>
|
||||
</div>
|
||||
|
||||
### Installation Guide
|
||||
|
||||
Depending on running in Docker (see the netbox-docker repository) add netbox_slm to the plugin_requirements.txt (requires a valid pypi version)
|
||||
## Installation Guide
|
||||
|
||||
```
|
||||
netbox-slm==0.1
|
||||
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``.
|
||||
|
||||
Note that this plugin is only tested against a single NetBox version at this time, see [Dockerfile-CI](https://github.com/ICTU/netbox_slm/blob/master/ci/Dockerfile-CI).
|
||||
|
||||
Next, follow these instructions (based on the NetBox docker variant [instructions](https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files)) to install the NetBox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/plugins.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
```dockerfile
|
||||
FROM netboxcommunity/netbox:vX.Y.Z
|
||||
|
||||
COPY ../pyproject.toml /tmp/
|
||||
RUN uv pip install -r /tmp/pyproject.toml
|
||||
```
|
||||
|
||||
Change the configuration.py to include the plugin:
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
```
|
||||
PLUGINS = [
|
||||
'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
|
||||
```
|
||||
|
||||
### Releasing Guide
|
||||
Now, build the image: ``docker compose build --no-cache``
|
||||
|
||||
_TODO_
|
||||
And finally, run NetBox with the SLM plugin: ``docker compose up``
|
||||
|
||||
### Developer Guide (local installation)
|
||||
|
||||
_Follow the steps below on your local system to run netbox and the netbox_slm plugin in developer mode_
|
||||
## Get in touch
|
||||
|
||||
### Setup
|
||||
|
||||
The goal below is to run all Netbox components in Docker and run a local Netbox Django copy with auto-reload to develop the plugin pointing to the Dockerized postgres and redis instances, basically ignoring the netbox docker runtime server.
|
||||
|
||||
### Gotcha's
|
||||
|
||||
_Netbox's exception handler seems to supress initial Exceptions, obfuscating missing imports or other Django related issues by re-raising an opague error, e.g. netbox_slm plugin namespace doesnotexist. It's recommended to often check the runtime server or patch Netbox's global exception handler._
|
||||
|
||||
### Steps
|
||||
|
||||
```
|
||||
from your projects directory clone the netbox repository
|
||||
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
$ git checkout f5356b8 (to pin 3.0.9)
|
||||
|
||||
install the virtual environment
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
|
||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
||||
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
````
|
||||
|
||||
The Netbox installation above will be used to run Django management commands like runserver, makemigrations and migrate, which will be explained in the next steps below;
|
||||
|
||||
```
|
||||
from your projects directory clone the netbox-slm repository
|
||||
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
````
|
||||
|
||||
This will start Netbox locally (requires Docker) and forward the redis and postgres ports to the localhost (make sure there's no processes using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
```
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
|
||||
or stop the stack with
|
||||
|
||||
$ docker-compose down
|
||||
|
||||
# to start fresh:
|
||||
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
```
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and redis connection strings (username, password) to the ones the netbox docker backend is using, for example (using default user and passwords from the netbox docker example):
|
||||
|
||||
```
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
```
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
```
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox-slm/src/
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
```
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
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).
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# NetBox SLM Security Policy
|
||||
|
||||
## Current status
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,9 @@
|
||||
StylesPath = styles
|
||||
|
||||
MinAlertLevel = suggestion
|
||||
Vocab = Base
|
||||
|
||||
Packages = proselint
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale, proselint
|
||||
@@ -0,0 +1,18 @@
|
||||
ARG PYTHON_VERSION=3.12
|
||||
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.2.3
|
||||
ARG PYTHON_VERSION=3.12
|
||||
|
||||
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM netboxcommunity/netbox:v4.2.3
|
||||
ARG PYTHON_VERSION=3.12
|
||||
|
||||
COPY ../pyproject.toml /tmp/
|
||||
RUN uv pip install -r /tmp/pyproject.toml --extra ci
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
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.2.3/netbox/netbox/configuration_testing.py
|
||||
"""
|
||||
from os import environ
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DATABASE = {
|
||||
'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'
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
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: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:ro
|
||||
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
depends_on:
|
||||
- netbox
|
||||
healthcheck:
|
||||
disable: true
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# based on https://github.com/netbox-community/netbox-docker/blob/3.2.0/docker-compose.yml
|
||||
services:
|
||||
netbox: &netbox
|
||||
image: netboxcommunity/netbox:${VERSION-v4.2.3}
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
env_file: env/netbox.env
|
||||
user: "unit:root"
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:8080/login/ || exit 1
|
||||
start_period: 90s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
volumes:
|
||||
- ./config:/etc/netbox/config: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
|
||||
netbox-housekeeping:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
netbox:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /opt/netbox/housekeeping.sh
|
||||
healthcheck:
|
||||
test: ps -aux | grep -v grep | grep -q housekeeping || exit 1
|
||||
start_period: 20s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
|
||||
# postgres
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
healthcheck:
|
||||
test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER
|
||||
start_period: 20s
|
||||
timeout: 30s
|
||||
interval: 10s
|
||||
retries: 5
|
||||
env_file: env/postgres.env
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
|
||||
# redis
|
||||
redis:
|
||||
image: valkey/valkey:8.0-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD
|
||||
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: valkey/valkey:8.0-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- valkey-server --requirepass $$REDIS_PASSWORD
|
||||
healthcheck: *redis-healthcheck
|
||||
env_file: env/redis-cache.env
|
||||
volumes:
|
||||
- netbox-redis-cache-data:/data
|
||||
|
||||
volumes:
|
||||
netbox-media-files:
|
||||
driver: local
|
||||
netbox-postgres-data:
|
||||
driver: local
|
||||
netbox-redis-cache-data:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
netbox-reports-files:
|
||||
driver: local
|
||||
netbox-scripts-files:
|
||||
driver: local
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
DB_HOST=postgres
|
||||
DB_NAME=netbox
|
||||
DB_PASSWORD=J5brHrAXFLQSif0K
|
||||
DB_USER=netbox
|
||||
EMAIL_FROM=netbox@bar.com
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_SERVER=localhost
|
||||
EMAIL_SSL_CERTFILE=
|
||||
EMAIL_SSL_KEYFILE=
|
||||
EMAIL_TIMEOUT=5
|
||||
EMAIL_USERNAME=netbox
|
||||
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
|
||||
EMAIL_USE_SSL=false
|
||||
EMAIL_USE_TLS=false
|
||||
GRAPHQL_ENABLED=true
|
||||
HOUSEKEEPING_INTERVAL=86400
|
||||
MAX_PAGE_SIZE=1000
|
||||
MEDIA_ROOT=/opt/netbox/netbox/media
|
||||
METRICS_ENABLED=false
|
||||
NAPALM_PASSWORD=
|
||||
NAPALM_TIMEOUT=10
|
||||
NAPALM_USERNAME=
|
||||
REDIS_CACHE_DATABASE=1
|
||||
REDIS_CACHE_HOST=redis-cache
|
||||
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
REDIS_CACHE_SSL=false
|
||||
REDIS_DATABASE=0
|
||||
REDIS_HOST=redis
|
||||
REDIS_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
REDIS_SSL=false
|
||||
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
|
||||
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
SKIP_SUPERUSER=false
|
||||
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
|
||||
SUPERUSER_EMAIL=admin@example.com
|
||||
SUPERUSER_NAME=admin
|
||||
SUPERUSER_PASSWORD=admin
|
||||
WEBHOOKS_ENABLED=true
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
POSTGRES_DB=netbox
|
||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
||||
POSTGRES_USER=netbox
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
Suchow
|
||||
Redistributions
|
||||
@@ -0,0 +1,3 @@
|
||||
docker composition
|
||||
Docker composition
|
||||
docker-composition
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Copyright 2022-2025 ICTU
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
__version__ = "1.8.1"
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
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 = {
|
||||
"top_level_menu": True,
|
||||
"link_cluster_installations": "right",
|
||||
"link_device_installations": "right",
|
||||
"link_virtualmachine_installations": "right",
|
||||
}
|
||||
|
||||
|
||||
config = SLMConfig
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
if settings.DEBUG:
|
||||
for model in apps.get_app_config("netbox_slm").get_models():
|
||||
admin.site.register(model)
|
||||
@@ -0,0 +1,124 @@
|
||||
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
display = 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 = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
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}"
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
"comments",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url")
|
||||
|
||||
def get_display(self, 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}"
|
||||
@@ -0,0 +1,18 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from netbox_slm.api.views import (
|
||||
NetboxSLMRootView,
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
SoftwareProductInstallationViewSet,
|
||||
SoftwareLicenseViewSet,
|
||||
)
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = NetboxSLMRootView
|
||||
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,50 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox_slm.api.serializers import (
|
||||
SoftwareProductSerializer,
|
||||
SoftwareProductVersionSerializer,
|
||||
SoftwareProductInstallationSerializer,
|
||||
SoftwareLicenseSerializer,
|
||||
)
|
||||
from netbox_slm.filtersets import (
|
||||
SoftwareProductFilterSet,
|
||||
SoftwareProductVersionFilterSet,
|
||||
SoftwareProductInstallationFilterSet,
|
||||
SoftwareLicenseFilterSet,
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
)
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilterSet
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilterSet
|
||||
|
||||
|
||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilterSet
|
||||
|
||||
|
||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
serializer_class = SoftwareLicenseSerializer
|
||||
filterset_class = SoftwareLicenseFilterSet
|
||||
@@ -0,0 +1,144 @@
|
||||
from django.db.models import Q
|
||||
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
|
||||
|
||||
from dcim.models import Device, Manufacturer
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
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()
|
||||
|
||||
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(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()
|
||||
|
||||
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(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()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
| 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)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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,20 @@
|
||||
# Generated by Django 3.2.9 on 2022-01-24 08:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0133_port_colors'),
|
||||
('netbox_slm', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='dcim.manufacturer'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 3.2.9 on 2022-04-07 12:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_slm', '0002_alter_softwareproduct_manufacturer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.2.9 on 2022-04-15 07:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0153_created_datetimefield'),
|
||||
('virtualization', '0029_created_datetimefield'),
|
||||
('netbox_slm', '0003_auto_20220407_1209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='version',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='virtualmachine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.1.8 on 2023-06-27 08:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('netbox_slm', '0004_auto_20220415_0705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SoftwareLicense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('type', models.CharField(max_length=128)),
|
||||
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expiration_date', models.DateField(blank=True, null=True)),
|
||||
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
|
||||
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-22 00:34
|
||||
|
||||
from django.db import migrations
|
||||
import netbox_slm.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_slm', '0005_add_software_license'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwarelicense',
|
||||
name='stored_location_url',
|
||||
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-04 10:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0036_virtualmachine_config_template'),
|
||||
('netbox_slm', '0006_softwarelicense_stored_location_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwarelicense',
|
||||
name='installation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='softwareproductinstallation',
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,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],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,181 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html, urlencode
|
||||
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)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
query_string = urlencode(dict(software_product_id=self.pk))
|
||||
search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list")
|
||||
# Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582
|
||||
return format_html(f"<a href='{search_target}?{query_string}'>{count}</a>") 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",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
query_string = urlencode(dict(version_id=self.pk))
|
||||
search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list")
|
||||
# Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582
|
||||
return format_html(f"<a href='{search_target}?{query_string}'>{count}</a>") if count else "0"
|
||||
|
||||
|
||||
class SoftwareProductInstallation(NetBoxModel):
|
||||
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
|
||||
)
|
||||
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
|
||||
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
|
||||
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk} ({self.platform})"
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name="%(app_label)s_%(class)s_platform",
|
||||
check=(
|
||||
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
|
||||
),
|
||||
violation_error_message="Installation requires exactly one platform destination.",
|
||||
)
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
return self.device or self.virtualmachine or self.cluster
|
||||
|
||||
def render_type(self):
|
||||
if self.device:
|
||||
return "device"
|
||||
if self.virtualmachine:
|
||||
return "virtualmachine"
|
||||
return "cluster"
|
||||
|
||||
|
||||
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 self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
||||
|
||||
@property
|
||||
def stored_location_txt(self):
|
||||
if self.stored_location_url and not self.stored_location:
|
||||
return "Link"
|
||||
return self.stored_location
|
||||
@@ -0,0 +1,91 @@
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
|
||||
from . import SLMConfig
|
||||
|
||||
slm_items = (
|
||||
PluginMenuItem(
|
||||
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",
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproduct"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
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",
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproductversion"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
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",
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
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
|
||||
@@ -0,0 +1,181 @@
|
||||
import django_tables2 as tables
|
||||
from django.db.models import Count, F, Value
|
||||
|
||||
from netbox.tables import NetBoxTable, ToggleColumn, columns
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count")
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
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")
|
||||
|
||||
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",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
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")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_platform(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True
|
||||
|
||||
def order_type(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
|
||||
|
||||
|
||||
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}"
|
||||
@@ -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_installation %}
|
||||
<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 %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product
|
||||
</h5>
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Installation
|
||||
</h5>
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Version
|
||||
</h5>
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class SlmBaseTestCase(TestCase):
|
||||
p_name: str = "test product"
|
||||
v_name: str = "test version"
|
||||
l_name: str = "test license"
|
||||
test_url: str = "https://github.com/ICTU/netbox_slm"
|
||||
vm: VirtualMachine
|
||||
software_product: SoftwareProduct
|
||||
software_product_version: SoftwareProductVersion
|
||||
software_product_installation: SoftwareProductInstallation
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
manufacturer = Manufacturer.objects.create(name="test manufacturer")
|
||||
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
|
||||
device_role = DeviceRole.objects.create(name="test device role")
|
||||
site = Site.objects.create(name="test site")
|
||||
cls.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
|
||||
cls.vm = VirtualMachine.objects.create(name="test VM")
|
||||
cluster_type = ClusterType.objects.create(name="test cluster type")
|
||||
cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
|
||||
|
||||
cls.software_product = SoftwareProduct.objects.create(name=cls.p_name)
|
||||
cls.software_product_version = SoftwareProductVersion.objects.create(
|
||||
name=cls.v_name, software_product=cls.software_product
|
||||
)
|
||||
cls.software_product_installation = SoftwareProductInstallation.objects.create(
|
||||
virtualmachine=cls.vm, software_product=cls.software_product, version=cls.software_product_version
|
||||
)
|
||||
cls.software_license = SoftwareLicense.objects.create(
|
||||
name=cls.l_name,
|
||||
software_product=cls.software_product,
|
||||
version=cls.software_product_version,
|
||||
installation=cls.software_product_installation,
|
||||
stored_location_url=cls.test_url,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
SoftwareLicense.objects.all().delete()
|
||||
SoftwareProductInstallation.objects.all().delete()
|
||||
SoftwareProductVersion.objects.all().delete()
|
||||
SoftwareProduct.objects.all().delete()
|
||||
Cluster.objects.all().delete()
|
||||
ClusterType.objects.all().delete()
|
||||
VirtualMachine.objects.all().delete()
|
||||
Device.objects.all().delete()
|
||||
Site.objects.all().delete()
|
||||
DeviceRole.objects.all().delete()
|
||||
DeviceType.objects.all().delete()
|
||||
Manufacturer.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
@@ -0,0 +1,51 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .base import SlmBaseTestCase
|
||||
|
||||
|
||||
class FunctionalTestCase(SlmBaseTestCase):
|
||||
"""Functional test cases that require a client and user with permissions"""
|
||||
|
||||
def setUp(self):
|
||||
test_user, _ = get_user_model().objects.get_or_create(username="test", is_superuser=True)
|
||||
self.client.force_login(test_user)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
get_user_model().objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
def test_template_content(self):
|
||||
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
|
||||
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
|
||||
self.assertContains(cluster_response, "</span> Add an installation")
|
||||
|
||||
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
|
||||
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
|
||||
self.assertContains(device_response, "</span> Add an installation")
|
||||
|
||||
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
|
||||
self.assertTemplateUsed(vm_response, "netbox_slm/installations_card_include.html")
|
||||
self.assertContains(vm_response, f'<a href="/plugins/slm/installations/add/?virtualmachine={self.vm.pk}"')
|
||||
|
||||
def test_setting_skip_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_virtualmachine_installations=None)))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"virtualmachine": None}):
|
||||
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
|
||||
self.assertTemplateNotUsed(vm_response, "netbox_slm/installations_card_include.html")
|
||||
|
||||
def test_setting_left_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_device_installations="left")))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"device": "left"}):
|
||||
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
|
||||
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
|
||||
self.assertContains(device_response, "</span> Add an installation")
|
||||
|
||||
def test_setting_full_content(self):
|
||||
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_cluster_installations="full")))
|
||||
with patch.dict("netbox_slm.template_content.installations", {"cluster": "full"}):
|
||||
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
|
||||
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
|
||||
self.assertContains(cluster_response, "</span> Add an installation")
|
||||
@@ -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)
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.urls import path, include
|
||||
|
||||
from netbox_slm import views # noqa F401
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
urlpatterns = [
|
||||
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"))),
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,83 @@
|
||||
[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",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
]
|
||||
dependencies = [
|
||||
"license-expression == 30.4.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
build = [
|
||||
"build == 1.2.2.post1",
|
||||
"setuptools == 75.8.0",
|
||||
"twine == 6.1.0",
|
||||
]
|
||||
ci = [
|
||||
"coverage == 7.6.11",
|
||||
"ruff == 0.9.6",
|
||||
]
|
||||
|
||||
[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"]
|
||||
@@ -0,0 +1,12 @@
|
||||
# Project metadata
|
||||
sonar.organization=ictu
|
||||
sonar.projectKey=ICTU_netbox_slm
|
||||
sonar.projectName=netbox-slm
|
||||
sonar.projectVersion=1.8.1
|
||||
|
||||
# Path is relative to the sonar-project.properties file
|
||||
sonar.sources=netbox_slm
|
||||
sonar.python.version=3.11, 3.12, 3.13
|
||||
|
||||
# Python test reports
|
||||
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
|
||||
@@ -1,17 +0,0 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = 'netbox_slm'
|
||||
verbose_name = 'Software Lifecycle Management'
|
||||
description = 'Software Lifecycle Management'
|
||||
version = '0.1'
|
||||
author = 'Hedde van der Heide'
|
||||
author_email = 'hedde.vanderheide@ictu.nl'
|
||||
base_url = 'slm'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'version_info': False
|
||||
}
|
||||
|
||||
config = SLMConfig
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductVersionInline(admin.TabularInline):
|
||||
model = SoftwareProductVersion
|
||||
fields = ('name',)
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(SoftwareProduct)
|
||||
class SoftwareProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
inlines = (SoftwareProductVersionInline,)
|
||||
@@ -1,40 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import PrimaryModelSerializer, OrganizationalModelSerializer
|
||||
# from netbox_slm.api.nested_serializers import (
|
||||
# NestedRecordSerializer,
|
||||
# NestedNameServerSerializer,
|
||||
# )
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductSerializer(PrimaryModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj.name
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(PrimaryModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj.name
|
||||
@@ -1,13 +0,0 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox_slm.api.views import (
|
||||
NetboxSLMRootView,
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
)
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = NetboxSLMRootView
|
||||
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
urlpatterns = router.urls
|
||||
@@ -1,44 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox_slm.api.serializers import SoftwareProductSerializer, SoftwareProductVersionSerializer
|
||||
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
"""
|
||||
NetboxSLM API root view
|
||||
"""
|
||||
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(CustomFieldModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilter
|
||||
|
||||
# @action(detail=True, methods=["get"])
|
||||
# def records(self, request, pk=None):
|
||||
# records = Record.objects.filter(zone=pk)
|
||||
# serializer = RecordSerializer(records, many=True, context={"request": request})
|
||||
# return Response(serializer.data)
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(CustomFieldModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilter
|
||||
|
||||
# @action(detail=True, methods=["get"])
|
||||
# def records(self, request, pk=None):
|
||||
# records = Record.objects.filter(zone=pk)
|
||||
# serializer = RecordSerializer(records, many=True, context={"request": request})
|
||||
# return Response(serializer.data)
|
||||
|
||||
# for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py
|
||||
@@ -1,50 +0,0 @@
|
||||
from django.forms import BoundField
|
||||
from django.urls import reverse
|
||||
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class CustomDynamicModelMultipleChoiceField(DynamicModelMultipleChoiceField):
|
||||
def get_bound_field(self, form, field_name):
|
||||
bound_field = BoundField(form, self, field_name)
|
||||
|
||||
# Set initial value based on prescribed child fields (if not already set)
|
||||
if not self.initial and self.initial_params:
|
||||
filter_kwargs = {}
|
||||
for kwarg, child_field in self.initial_params.items():
|
||||
value = form.initial.get(child_field.lstrip("$"))
|
||||
if value:
|
||||
filter_kwargs[kwarg] = value
|
||||
if filter_kwargs:
|
||||
self.initial = self.queryset.filter(**filter_kwargs).first()
|
||||
|
||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||
# will be populated on-demand via the APISelect widget.
|
||||
data = bound_field.value()
|
||||
|
||||
if data:
|
||||
field_name = getattr(self, "to_field_name") or "pk"
|
||||
filter = self.filter(field_name=field_name)
|
||||
try:
|
||||
self.queryset = filter.filter(self.queryset, data)
|
||||
except (TypeError, ValueError):
|
||||
# Catch any error caused by invalid initial data passed from the user
|
||||
self.queryset = self.queryset.none()
|
||||
else:
|
||||
self.queryset = self.queryset.none()
|
||||
|
||||
# Set the data URL on the APISelect widget (if not already set)
|
||||
widget = bound_field.field.widget
|
||||
if not widget.attrs.get("data-url"):
|
||||
app_label = self.queryset.model._meta.app_label
|
||||
model_name = self.queryset.model._meta.model_name
|
||||
|
||||
#
|
||||
# Custom url for work with plugin api
|
||||
#
|
||||
data_url = reverse(
|
||||
"plugins-api:{}-api:{}-list".format(app_label, model_name)
|
||||
)
|
||||
widget.attrs["data-url"] = data_url
|
||||
|
||||
return bound_field
|
||||
@@ -1,75 +0,0 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import PrimaryModelFilterSet
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class BaseFilter(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductFilter(BaseFilter):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",) # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilter(BaseFilter):
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains", label="Version"
|
||||
)
|
||||
# software_product_id = django_filters.ModelMultipleChoiceFilter(
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# label='SoftwareProduct (ID)',
|
||||
# )
|
||||
# software_product = django_filters.ModelMultipleChoiceFilter(
|
||||
# field_name='software_product__name',
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# to_field_name='name',
|
||||
# label='SoftwareProduct (name)',
|
||||
# )
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product") # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilter(BaseFilter):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple() # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(software_product__name__icontains=value) | Q(version__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -1,202 +0,0 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.forms import (
|
||||
CustomFieldModelForm,
|
||||
CustomFieldModelCSVForm,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm,
|
||||
CustomFieldModelFilterForm,
|
||||
)
|
||||
from dcim.models import Manufacturer, Device
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description",) # "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProduct instances."""
|
||||
|
||||
model = SoftwareProduct
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product",) # "tags")
|
||||
|
||||
# def clean(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
# return super(SoftwareProductVersionForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProductVersion instances."""
|
||||
|
||||
model = SoftwareProductVersion
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductVersionCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
|
||||
),
|
||||
query_params={
|
||||
'software_product': '$software_product',
|
||||
}
|
||||
)
|
||||
|
||||
# todo need version and device ?
|
||||
|
||||
# tags = DynamicModelMultipleChoiceField(
|
||||
# queryset=Tag.objects.all(),
|
||||
# required=False,
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("device", "virtualmachine", "software_product", "version",) # "tags")
|
||||
|
||||
# def clean(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
# return super(SoftwareProductInstallationForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
"""Form for filtering SoftwareProductInstallation instances."""
|
||||
|
||||
model = SoftwareProductInstallation
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductInstallationCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -1,97 +0,0 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import safestring
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProduct(PrimaryModel):
|
||||
"""
|
||||
A SoftwareProduct represents ...
|
||||
"""
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='software_products'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductVersion(PrimaryModel):
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_versions'
|
||||
)
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductInstallation(PrimaryModel):
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_installations',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
virtualmachine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_installations',
|
||||
null = True,
|
||||
blank = True
|
||||
)
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='software_products'
|
||||
)
|
||||
version = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProductVersion',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='softwareproduct_versions'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
@@ -1,65 +0,0 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproduct_list',
|
||||
link_text='Software Products',
|
||||
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",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductversion_list',
|
||||
link_text='Versions',
|
||||
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",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductinstallation_list',
|
||||
link_text='Installations',
|
||||
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",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -1,125 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn
|
||||
|
||||
|
||||
class SoftwareProductTable(BaseTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(BaseTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Version')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('software_product__manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
# "tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(BaseTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
device = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True
|
||||
)
|
||||
virtualmachine = tables.Column(
|
||||
accessor=Accessor('virtualmachine'),
|
||||
linkify=True
|
||||
)
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
version = tables.Column(
|
||||
accessor=Accessor('version'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
# tags = TagColumn(
|
||||
# url_name="plugins:netbox_dns:zone_list",
|
||||
# )
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"software_product",
|
||||
"version",
|
||||
# "tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"software_product",
|
||||
"version",
|
||||
# "tags",
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load custom_links %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load view_helpers %}
|
||||
|
||||
{% block header %}
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|plugin_viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
{% endblock %}
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ object }}{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
<div class="object-subtitle">
|
||||
<span>Created {{ object.created|annotated_date }}</span>
|
||||
<span class="separator">·</span>
|
||||
<span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block controls %}
|
||||
{# Clone/Edit/Delete Buttons #}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% plugin_buttons object %}
|
||||
|
||||
{# Extra buttons #}
|
||||
{% block extra_controls %}{% endblock %}
|
||||
|
||||
{% if object.clone_fields and request.user|can_add:object %}
|
||||
{% plugin_clone_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_change:object %}
|
||||
{% plugin_edit_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% plugin_delete_button object %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="control-group">
|
||||
{% custom_links object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
||||
</li>
|
||||
{% endblock tab_items %}
|
||||
{% if perms.extras.view_journalentry %}
|
||||
{% with journal_viewname=object|plugin_viewname:'journal' %}
|
||||
{% url journal_viewname pk=object.pk as journal_url %}
|
||||
{% if journal_url %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
|
||||
Journal {% badge object.journal_entries.count %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if perms.extras.view_objectchange %}
|
||||
{% with changelog_viewname=object|plugin_viewname:'changelog' %}
|
||||
{% url changelog_viewname pk=object.pk as changelog_url %}
|
||||
{% if changelog_url %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,141 +0,0 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load view_helpers %}
|
||||
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% block extra_controls %}{% endblock %}
|
||||
{% if permissions.add and 'add' in action_buttons %}
|
||||
{% add_button content_type.model_class|plugin_viewname:"add" %}
|
||||
{% endif %}
|
||||
{% if permissions.add and 'import' in action_buttons %}
|
||||
{% import_button content_type.model_class|plugin_viewname:"import" %}
|
||||
{% endif %}
|
||||
{% if 'export' in action_buttons %}
|
||||
{% export_button content_type %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
|
||||
{% badge table.page.paginator.count %}
|
||||
</button>
|
||||
</li>
|
||||
{% if filter_form %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
Filters
|
||||
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock tab_items %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
|
||||
{# Object list #}
|
||||
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
|
||||
{# Applied filters #}
|
||||
{% if filter_form %}
|
||||
{% applied_filters filter_form request.GET %}
|
||||
{% endif %}
|
||||
|
||||
{# "Select all" form #}
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
|
||||
<div id="select-all-box" class="d-none card noprint">
|
||||
<form method="post" class="form col-md-12">
|
||||
{% csrf_token %}
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
{% if bulk_edit_url and permissions.change %}
|
||||
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bulk_delete_url and permissions.delete %}
|
||||
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{# Object table controls #}
|
||||
{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
|
||||
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
|
||||
{# Object table #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form buttons #}
|
||||
{% if permissions.change or permissions.delete %}
|
||||
{% with bulk_edit_url=content_type.model_class|plugin_validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|plugin_validated_viewname:"bulk_delete" %}
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% block bulk_buttons %}{% endblock %}
|
||||
{% if bulk_edit_url and permissions.change %}
|
||||
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bulk_delete_url and permissions.delete %}
|
||||
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
{# Paginator #}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
|
||||
{# Filter form #}
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Table config form #}
|
||||
{% table_config_form table table_name="ObjectTable" %}
|
||||
{% endblock content-wrapper %}
|
||||
@@ -1,76 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
{% for version in object.softwareproduct_versions.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Versions</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">NAME</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in versions %}
|
||||
<tr>
|
||||
<td>{{ version.name|truncatechars:32 }}</td>
|
||||
<td class="noprint text-end text-nowrap">
|
||||
{% if request.user|can_change:object %}
|
||||
{% plugin_edit_button version %}
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% plugin_delete_button version %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,82 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">bla</th>
|
||||
<td>
|
||||
<!-- {% for version in object.softwareproduct_versions.all %}-->
|
||||
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
|
||||
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
|
||||
<!-- </a>-->
|
||||
<!-- {% endfor %}-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,82 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Software Product
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>
|
||||
<!-- {% for version in object.softwareproduct_versions.all %}-->
|
||||
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
|
||||
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
|
||||
<!-- </a>-->
|
||||
<!-- {% endfor %}-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,91 +0,0 @@
|
||||
{% extends 'netbox_slm/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load view_helpers %}
|
||||
{% load perms %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% comment %}
|
||||
<a href="{% url 'plugins:netbox_slm:record_add' %}?zone={{ object.id }}" class="btn btn-sm btn-primary" role="button">
|
||||
<i class="mdi mdi-plus-thick"></i> Add Record
|
||||
</a>
|
||||
{% endcomment %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Zone
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
<ul>
|
||||
<!-- {% for nameserver in object.nameservers.all %}-->
|
||||
<!-- <li>-->
|
||||
<!-- <a href="{% url 'plugins:netbox_slm:nameserver' pk=nameserver.pk %}">-->
|
||||
<!-- {{ nameserver }}-->
|
||||
<!-- </a>-->
|
||||
<!-- </li>-->
|
||||
<!-- {% endfor %}-->
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">-->
|
||||
<!-- <div class="col col-md-12">-->
|
||||
<!-- <div class="card">-->
|
||||
<!-- <h5 class="card-header">Records</h5>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <table class="table table-hover attr-table table-striped">-->
|
||||
<!-- <thead>-->
|
||||
<!-- <tr>-->
|
||||
<!-- <th scope="col">TYPE</th>-->
|
||||
<!-- <th scope="col">NAME</th>-->
|
||||
<!-- <th scope="col">VALUE</th>-->
|
||||
<!-- <th scope="col">TTL</th>-->
|
||||
<!-- <th scope="col">Actions</th>-->
|
||||
<!-- </tr>-->
|
||||
<!-- </thead>-->
|
||||
<!-- <tbody>-->
|
||||
<!-- {% for record in records %}-->
|
||||
<!-- <tr>-->
|
||||
<!-- <td>{{ record.type }}</td>-->
|
||||
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
|
||||
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
|
||||
<!-- <td>{{ record.ttl }}</td>-->
|
||||
<!-- <td class="noprint text-end text-nowrap">-->
|
||||
<!-- {% if request.user|can_change:object %}-->
|
||||
<!-- {% plugin_edit_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% if request.user|can_delete:object %}-->
|
||||
<!-- {% plugin_delete_button record %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </tbody>-->
|
||||
<!-- </table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
{% endblock %}
|
||||
@@ -1,94 +0,0 @@
|
||||
from django import template
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django import template
|
||||
|
||||
from utilities.templatetags.buttons import _get_viewname
|
||||
from utilities.utils import prepare_cloned_fields
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter()
|
||||
def plugin_validated_viewname(model, action):
|
||||
"""Return the view name for :
|
||||
* the given model and action if valid
|
||||
* or the given model and action if valid inside a plugin
|
||||
* or None if invalid.
|
||||
"""
|
||||
core_viewname = f"{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
plugin_viewname = (
|
||||
f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
)
|
||||
try:
|
||||
# Validate and return the view name. We don't return the actual URL yet because many of the templates
|
||||
# are written to pass a name to {% url %}.
|
||||
reverse(core_viewname)
|
||||
return core_viewname
|
||||
except NoReverseMatch:
|
||||
try:
|
||||
reverse(plugin_viewname)
|
||||
return plugin_viewname
|
||||
except NoReverseMatch:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter()
|
||||
def plugin_viewname(model, action):
|
||||
"""
|
||||
Return the view name for the given model and action. Does not perform any validation.
|
||||
"""
|
||||
return f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
|
||||
|
||||
|
||||
#
|
||||
# Instance buttons
|
||||
#
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/clone.html")
|
||||
def plugin_clone_button(instance):
|
||||
viewname = _get_viewname(instance, "add")
|
||||
url = reverse(f"plugins:{viewname}")
|
||||
|
||||
# Populate cloned field values
|
||||
param_string = prepare_cloned_fields(instance)
|
||||
if param_string:
|
||||
url = f"{url}?{param_string}"
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/edit.html")
|
||||
def plugin_edit_button(instance, **kwargs):
|
||||
viewname = _get_viewname(instance, "edit")
|
||||
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/delete.html")
|
||||
def plugin_delete_button(instance, **kwargs):
|
||||
viewname = _get_viewname(instance, "delete")
|
||||
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# List buttons
|
||||
#
|
||||
|
||||
|
||||
@register.inclusion_tag("buttons/add.html")
|
||||
def plugin_add_button(url):
|
||||
url = reverse(f"plugins:{url}")
|
||||
|
||||
return {
|
||||
"add_url": url,
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
from django.urls import path
|
||||
from extras.views import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
|
||||
urlpatterns = [
|
||||
# Software Products
|
||||
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
||||
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
||||
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
||||
path(
|
||||
"software-products/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproduct_changelog",
|
||||
kwargs={"model": SoftwareProduct},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
|
||||
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
||||
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), name="softwareproductversion_bulk_delete"),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
||||
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
||||
path(
|
||||
"versions/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductversion_changelog",
|
||||
kwargs={"model": SoftwareProductVersion},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("installations/", views.SoftwareProductInstallationListView.as_view(), name='softwareproductinstallation_list'),
|
||||
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), name='softwareproductinstallation_add'),
|
||||
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), name="softwareproductinstallation_import"),
|
||||
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), name="softwareproductinstallation_bulk_edit"),
|
||||
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_bulk_delete"),
|
||||
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(), name="softwareproductinstallation"),
|
||||
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(), name="softwareproductinstallation_delete"),
|
||||
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(), name="softwareproductinstallation_edit"),
|
||||
path(
|
||||
"installations/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductinstallation_changelog",
|
||||
kwargs={"model": SoftwareProductInstallation},
|
||||
),
|
||||
]
|
||||
@@ -1,165 +0,0 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter
|
||||
from netbox_slm.forms import (
|
||||
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm,
|
||||
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
|
||||
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
)
|
||||
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
filterset_form = SoftwareProductFilterForm
|
||||
table = SoftwareProductTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproduct_versions.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = SoftwareProductCSVForm
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
table = SoftwareProductTable
|
||||
form = SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
filterset_form = SoftwareProductVersionFilterForm
|
||||
table = SoftwareProductVersionTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
# def get_extra_context(self, request, instance):
|
||||
# records = instance.record_set.all()
|
||||
# return {"records": records}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = SoftwareProductVersionCSVForm
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
table = SoftwareProductVersionTable
|
||||
form = SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
filterset_form = SoftwareProductInstallationFilterForm
|
||||
table = SoftwareProductInstallationTable
|
||||
template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
# def get_extra_context(self, request, instance):
|
||||
# records = instance.record_set.all()
|
||||
# return {"records": records}
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = SoftwareProductInstallationCSVForm
|
||||
table = SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
table = SoftwareProductInstallationTable
|
||||
form = SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = SoftwareProductInstallationTable
|
||||
@@ -1,14 +0,0 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='netbox-slm',
|
||||
version='0.9',
|
||||
description='Software Lifecycle Management Netbox Plugin',
|
||||
url='https://github.com/ICTU/netbox_slm',
|
||||
author='Hedde van der Heide',
|
||||
license='Apache 2.0',
|
||||
install_requires=[],
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
git clone -b release https://github.com/netbox-community/netbox-docker.git
|
||||
cd netbox-docker
|
||||
tee docker-compose.override.yml <<EOF
|
||||
version: '3.4'
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
|
||||
Reference in New Issue
Block a user