Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d62f26b2 | ||
|
|
35ddffc903 | ||
|
|
919b2d01e5 | ||
|
|
157c891cde | ||
|
|
3599dc703e | ||
|
|
294e20b891 | ||
|
|
9f3bddfbb1 | ||
|
|
71a5db0ece | ||
|
|
adfc2ae72c | ||
|
|
5887129546 | ||
|
|
7ca670c717 | ||
|
|
e1c817229b | ||
|
|
01d40ddecc | ||
|
|
8a2dabc382 | ||
|
|
f84a6bde2a | ||
|
|
811490e429 | ||
|
|
7ce718ff9d | ||
|
|
9e87afcde7 | ||
|
|
e03195c9b1 | ||
|
|
59730a5184 | ||
|
|
69594bc33a | ||
|
|
766ecfc25e | ||
|
|
d773a407a2 | ||
|
|
175a01b257 | ||
|
|
334a76e640 | ||
|
|
ef45789b30 | ||
|
|
4bd02ab6f9 | ||
|
|
410b8f6617 | ||
|
|
306d853a16 | ||
|
|
193f04a208 | ||
|
|
c3ed952b5f | ||
|
|
87fdeb1c24 | ||
|
|
a0a98bb407 | ||
|
|
3539539c4e | ||
|
|
72646a6287 | ||
|
|
86215f43a6 | ||
|
|
230b3b5115 |
@@ -1,36 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build the Docker image
|
|
||||||
run: docker compose build --no-cache
|
|
||||||
|
|
||||||
- name: Verify the Docker image
|
|
||||||
# clearcache is provided by Django extras, which is not loaded if netbox-slm cannot be loaded
|
|
||||||
run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py clearcache"
|
|
||||||
|
|
||||||
- name: Run Django tests with coverage for netbox_slm
|
|
||||||
run: |
|
|
||||||
chmod go+w ci/reports
|
|
||||||
docker compose run netbox sh -c " \
|
|
||||||
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm && \
|
|
||||||
/opt/netbox/venv/bin/coverage report --fail-under=0 && \
|
|
||||||
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
|
|
||||||
|
|
||||||
- name: Sonarcloud scan
|
|
||||||
if: env.SONAR_TOKEN != null
|
|
||||||
uses: sonarsource/sonarcloud-github-action@master
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Python
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4.0.0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.9"
|
|
||||||
|
|
||||||
- name: Install tools
|
|
||||||
run: pip install .[tools]
|
|
||||||
|
|
||||||
- name: Run ruff
|
|
||||||
run: ruff netbox_slm
|
|
||||||
|
|
||||||
- name: Run black
|
|
||||||
run: black netbox_slm
|
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Make the required single platform destination explicit when creating an installation
|
||||||
|
* Fix installation form submission on NetBox v4.6.5 when form mixins return no cleaned-data value
|
||||||
|
* Prevent the notification log creation flag from shadowing Django's translation function during email delivery
|
||||||
|
* Use NetBox's configured `EMAIL['FROM_EMAIL']` value as the SMTP sender instead of Django's localhost fallback
|
||||||
|
* Register the expiration event with a concrete string so NetBox 4.6 can render the notification dropdown
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Optional tenant group and tenant assignments for software licenses
|
||||||
|
* German translation for navigation, forms, tables, detail views, choices, and validation messages
|
||||||
|
* Optional lifetime or recurring renewal interval for licenses
|
||||||
|
* Optional license file, license key, and provider portal URL
|
||||||
|
* Daily license expiration notifications for users who subscribe to a license
|
||||||
|
* Default reminders one month and one week before expiration plus an optional custom interval
|
||||||
|
* Optional SMTP email in addition to mandatory NetBox notifications
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v4.6.5
|
||||||
|
* Replace the Docker-based setup with Git/Pip installation documentation
|
||||||
|
* Render tenant group and tenant in a dedicated license form section
|
||||||
|
* Bump the package version so Git/Pip upgrades replace older installations
|
||||||
|
* Make the license type field optional
|
||||||
|
* Show the plugin under Licences in the main menu
|
||||||
|
* Restrict SMTP notification activation to administrators and explicitly authorized users
|
||||||
|
* Repeat NetBox and optional email reminders on every daily job run from the first due interval through expiration
|
||||||
|
|
||||||
|
## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v4.6.3 (#76)
|
||||||
|
* Support Python 3.14 (#76)
|
||||||
|
|
||||||
|
## [1.8.3](https://github.com/ICTU/netbox_slm/releases/tag/1.8.3) - 2025-12-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Filters with default order by ID (#75)
|
||||||
|
|
||||||
|
## [1.8.2](https://github.com/ICTU/netbox_slm/releases/tag/1.8.2) - 2025-06-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Permission check for adding software product installations (#70)
|
||||||
|
|
||||||
|
## [1.8.1](https://github.com/ICTU/netbox_slm/releases/tag/1.8.1) - 2025-04-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Filtering on foreign objects (#65)
|
||||||
|
|
||||||
|
## [1.8.0](https://github.com/ICTU/netbox_slm/releases/tag/1.8.0) - 2025-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Toggle to show plugin as top level menu item (#42)
|
||||||
|
* Bulk import and bulk edit (#4)
|
||||||
|
* Changelog with backdated changes (#51)
|
||||||
|
* List filtering (#54)
|
||||||
|
* Show installations on native objects (#49)
|
||||||
|
* SPDX license expressions (#47)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v4.2.3 (#51)
|
||||||
|
* Render objects consistently (#58)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Model detail view listing of linked objects (#55)
|
||||||
|
* Search link of installations table cells (#48)
|
||||||
|
|
||||||
|
## [1.7.0](https://github.com/ICTU/netbox_slm/releases/tag/1.7.0) - 2024-07-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v4.0.6 (#38)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Hidden plugin data when not logged in (#37)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
* Broken bulk import and bulk edit (#4)
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/ICTU/netbox_slm/releases/tag/1.6.0) - 2024-02-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Additional `SoftwareProductVersion` fields (#10)
|
||||||
|
* Additional `SoftwareLicense` fields (#26)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v3.7.2 (#35)
|
||||||
|
|
||||||
|
## [1.5](https://github.com/ICTU/netbox_slm/releases/tag/1.5) - 2023-10-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Hyperlink option for `SoftwareLicense.stored_location` (#27)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Update docs and imports for newer NetBox versions (#23)
|
||||||
|
* Support NetBox v3.6.1 (#21)
|
||||||
|
* Allow linking `SoftwareProductInstallation` to `Cluster` (#17)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Date picker for start and expiration dates (#22)
|
||||||
|
|
||||||
|
## [1.4](https://github.com/ICTU/netbox_slm/releases/tag/1.4) - 2023-06-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* License information for software installations (#8)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Replace `NetBoxModelCSVForm` with `NetBoxModelImportForm` (#18)
|
||||||
|
|
||||||
|
## [1.3](https://github.com/ICTU/netbox_slm/releases/tag/1.3) - 2023-04-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Set up continuous integration (#3)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Support NetBox v3.4.7 (#14)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Unify API serialization (#12)
|
||||||
|
|
||||||
|
## [1.2](https://github.com/ICTU/netbox_slm/releases/tag/1.2) - 2022-05-19
|
||||||
|
|
||||||
|
* Fixed filtering of relevant versions when adding a new installation
|
||||||
|
* Manufacturer is now required for the add software product form
|
||||||
|
|
||||||
|
## [1.1](https://github.com/ICTU/netbox_slm/releases/tag/1.1) - 2022-05-03
|
||||||
|
|
||||||
|
* Fixed search filter
|
||||||
@@ -1,13 +1,201 @@
|
|||||||
Copyright 2022 ICTU
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
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
|
1. Definitions.
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
limitations under the License.
|
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.
|
||||||
|
|||||||
@@ -1,222 +1,160 @@
|
|||||||
<h1 align="center">NetBox SLM</h1>
|
# NetBox SLM
|
||||||
|
|
||||||
<p align="center"><i>Netbox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
|
NetBox SLM erweitert NetBox um die Verwaltung von Softwareprodukten, Versionen, Installationen und Lizenzen.
|
||||||
|
Lizenzen können optional einer NetBox-Mandantengruppe und einem Mandanten zugeordnet werden.
|
||||||
|
|
||||||
<div align="center">
|
Diese Variante ist für **NetBox 4.6.5** vorgesehen.
|
||||||
<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>
|
|
||||||
|
|
||||||
|
Die Oberfläche ist auf Englisch und Deutsch verfügbar. Die Sprache richtet sich nach der in NetBox für den
|
||||||
|
Benutzer ausgewählten Sprache.
|
||||||
|
|
||||||
## Known Issues
|
## Funktionen
|
||||||
|
|
||||||
- WARNING: This plugin is only tested with a single Netbox version at this time.
|
- Softwareprodukte und Hersteller verwalten
|
||||||
- CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
|
- Versionen, Release-Typen und Supportzeiträume erfassen
|
||||||
|
- Installationen Geräten, virtuellen Maschinen oder Clustern zuordnen
|
||||||
|
- Softwarelizenzen einschließlich Laufzeit, Umfang und Ablageort verwalten
|
||||||
|
- Lifetime-Lizenzen oder Erneuerungsintervalle in Tagen, Monaten oder Jahren erfassen
|
||||||
|
- Lizenzdateien, Lizenzschlüssel und Links zum Anbieterportal hinterlegen
|
||||||
|
- Ablaufwarnungen standardmäßig einen Monat und eine Woche vor Lizenzende
|
||||||
|
- Zusätzliche eigene Vorwarnzeit in Tagen, Wochen oder Monaten
|
||||||
|
- NetBox-Benachrichtigungen und optionaler E-Mail-Versand über die NetBox-SMTP-Konfiguration
|
||||||
|
- Lizenzen Mandantengruppen und Mandanten zuordnen
|
||||||
|
- Bedienung über die NetBox-Oberfläche und REST-API
|
||||||
|
- Filter, Bulk-Import und Bulk-Bearbeitung
|
||||||
|
|
||||||
## Installation Guide
|
> **Hinweis:** Lizenzschlüssel werden in der NetBox-Datenbank und Lizenzdateien im konfigurierten NetBox-
|
||||||
|
> Medienverzeichnis gespeichert. Der Zugriff auf Datenbank, REST-API und Medienverzeichnis sollte entsprechend
|
||||||
|
> geschützt und gesichert werden.
|
||||||
|
|
||||||
When using the Docker version of Netbox, first follow the netbox-docker [quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the netbox-docker repo and set up the ``docker-compose.override.yml``.
|
## Installation
|
||||||
|
|
||||||
Next, follow these instructions (based on the Netbox docker variant
|
NetBox installiert lokale Erweiterungen aus `/opt/netbox/local_requirements.txt`. Die Git-URL muss dort dauerhaft
|
||||||
[instructions](https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files))
|
eingetragen werden, damit das Plugin bei einem späteren NetBox-Upgrade erneut installiert wird.
|
||||||
to install the Netbox SLM plugin:
|
|
||||||
|
|
||||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
### 1. Git-Repository als lokale Anforderung eintragen
|
||||||
``configuration/plugins.py``.
|
|
||||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
|
|
||||||
contents.
|
|
||||||
3. Create a ``Dockerfile-SLM`` with contents:
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
FROM netboxcommunity/netbox:vX.Y.Z
|
echo "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" >> /opt/netbox/local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
COPY ./plugin_requirements.txt /
|
Vorher kann geprüft werden, ob der Eintrag bereits vorhanden ist:
|
||||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create a ``docker-compose.override.yml`` with contents:
|
```bash
|
||||||
|
grep -qxF "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" /opt/netbox/local_requirements.txt || \
|
||||||
|
echo "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" >> /opt/netbox/local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
```
|
### 2. Plugin mit Pip installieren
|
||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
netbox:
|
|
||||||
ports:
|
|
||||||
- 8000:8080
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile-SLM
|
|
||||||
image: netbox:slm
|
|
||||||
netbox-worker:
|
|
||||||
image: netbox:slm
|
|
||||||
netbox-housekeeping:
|
|
||||||
image: netbox:slm
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, build the image: ``docker compose build --no-cache``
|
Die Installation muss im Python-Virtual-Environment von NetBox erfolgen:
|
||||||
|
|
||||||
And finally, run Netbox with the SLM plugin: ``docker compose up``
|
```bash
|
||||||
|
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
||||||
|
"git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git"
|
||||||
|
```
|
||||||
|
|
||||||
## Releasing Guide
|
Alternativ können alle lokalen Anforderungen installiert werden:
|
||||||
|
|
||||||
To draft a release;
|
```bash
|
||||||
|
/opt/netbox/venv/bin/pip install -r /opt/netbox/local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
update the `netbox_slm/__init__.py` file to reflect the new version, then from the *src*
|
Die installierte Version kann anschließend geprüft werden:
|
||||||
directory run
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ python -m build
|
/opt/netbox/venv/bin/pip show netbox-slm
|
||||||
$ twine upload dist/*
|
```
|
||||||
```
|
|
||||||
|
|
||||||
On Github.com create a similar tag and version. These steps could be
|
Für diese Variante muss dort mindestens Version `1.12.0` stehen.
|
||||||
automated with a github workflow.
|
|
||||||
|
|
||||||
|
## Ablaufbenachrichtigungen
|
||||||
|
|
||||||
## Developer Guide (local installation)
|
Für Lizenzen mit Ablaufdatum sind Benachrichtigungen standardmäßig aktiviert. Die Standardtermine liegen einen
|
||||||
|
Monat und eine Woche vor dem Ablaufdatum. Pro Lizenz kann zusätzlich eine eigene Vorwarnzeit festgelegt oder die
|
||||||
|
Standardtermine deaktiviert werden.
|
||||||
|
|
||||||
*Follow the steps below on your local system to run netbox and the
|
Empfänger entscheiden selbst, ob sie informiert werden: Vertrieb, zuständiger Techniker oder andere Benutzer öffnen
|
||||||
netbox_slm plugin in developer mode*
|
die Lizenz und wählen die NetBox-Aktion **Abonnieren**. Mit **Abonnement beenden** können sie die Meldungen wieder
|
||||||
|
abschalten. Ohne Abonnenten wird keine Nachricht versendet.
|
||||||
|
|
||||||
### Setup
|
Die NetBox-Nachricht wird immer erzeugt. Wird an der Lizenz zusätzlich **E-Mail-Benachrichtigungen** aktiviert, sendet
|
||||||
|
das Plugin auch eine E-Mail an die im Benutzerkonto hinterlegte Adresse. Dafür muss der SMTP-Versand in NetBox
|
||||||
|
konfiguriert sein.
|
||||||
|
|
||||||
The goal below is to run all Netbox components in Docker and run a local
|
Die Aktivierung des E-Mail-Versands darf nur durch Administratoren oder Benutzer mit der Berechtigung
|
||||||
Netbox Django copy with auto-reload to develop the plugin pointing to
|
`netbox_slm.manage_softwarelicense_email_notifications` geändert werden. Die eigentlichen SMTP-Zugangsdaten werden
|
||||||
the Dockerized postgres and redis instances, basically ignoring the
|
ausschließlich in der NetBox-Serverkonfiguration gepflegt und sind im Plugin nicht einsehbar.
|
||||||
netbox docker runtime server.
|
|
||||||
|
|
||||||
### Steps
|
Die Prüfung läuft einmal täglich als NetBox-Systemjob. Sobald der erste konfigurierte Erinnerungstermin erreicht ist,
|
||||||
|
werden die Abonnenten bei jedem Joblauf erneut informiert: immer über NetBox und bei aktivierter E-Mail-Option
|
||||||
|
zusätzlich per SMTP. Dies wird bis einschließlich des Ablaufdatums wiederholt. Der NetBox-RQ-Worker muss dafür mit
|
||||||
|
Scheduler-Unterstützung laufen; dies ist bei der regulären NetBox-Installation mit `rqworker` standardmäßig der Fall.
|
||||||
|
|
||||||
from your projects directory clone the netbox repository
|
### 3. Plugin in NetBox aktivieren
|
||||||
|
|
||||||
```
|
In `/opt/netbox/netbox/netbox/configuration.py` ergänzen:
|
||||||
$ git clone https://github.com/netbox-community/netbox
|
|
||||||
$ cd netbox
|
|
||||||
```
|
|
||||||
|
|
||||||
install the virtual environment
|
|
||||||
|
|
||||||
```
|
|
||||||
$ pipenv shell
|
|
||||||
$ pipenv install
|
|
||||||
```
|
|
||||||
|
|
||||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
```python
|
||||||
|
PLUGINS = [
|
||||||
```
|
"netbox_slm",
|
||||||
DEBUG = True
|
]
|
||||||
SECRET_KEY = 'dummy'
|
```
|
||||||
DEVELOPER = True
|
|
||||||
PLUGINS = [
|
|
||||||
'netbox_slm',
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
The Netbox installation above will be used to run Django management
|
Falls bereits andere Plugins eingetragen sind, wird `"netbox_slm"` lediglich zur vorhandenen Liste hinzugefügt.
|
||||||
commands like runserver, makemigrations and migrate, which will be
|
Eine zusätzliche `PLUGINS_CONFIG`-Konfiguration ist für den Standardbetrieb nicht erforderlich.
|
||||||
explained in the next steps below;
|
|
||||||
|
|
||||||
from your projects directory clone the netbox-slm repository
|
### 4. Datenbank migrieren und statische Dateien aktualisieren
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ git clone https://github.com/ICTU/netbox_slm
|
cd /opt/netbox/netbox
|
||||||
$ cd netbox_slm
|
/opt/netbox/venv/bin/python manage.py migrate
|
||||||
$ ./start-netbox.sh
|
/opt/netbox/venv/bin/python manage.py collectstatic --no-input
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
### 5. NetBox-Dienste neu starten
|
||||||
|
|
||||||
```
|
Bei einer üblichen systemd-Installation:
|
||||||
$ cd netbox-docker
|
|
||||||
$ docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
or stop the stack with
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ docker-compose down
|
sudo systemctl restart netbox netbox-rq
|
||||||
```
|
```
|
||||||
|
|
||||||
# to start fresh:
|
|
||||||
|
|
||||||
```
|
Je nach Installation können die Namen der Dienste abweichen.
|
||||||
$ 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
|
## Aktualisierung
|
||||||
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):
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
<<collapsed>>
|
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
||||||
|
"git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git"
|
||||||
|
cd /opt/netbox/netbox
|
||||||
|
/opt/netbox/venv/bin/python manage.py migrate
|
||||||
|
/opt/netbox/venv/bin/python manage.py collectstatic --no-input
|
||||||
|
sudo systemctl restart netbox netbox-rq
|
||||||
|
```
|
||||||
|
|
||||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
Der Eintrag in `/opt/netbox/local_requirements.txt` bleibt dabei bestehen.
|
||||||
# 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
|
## Deinstallation
|
||||||
# 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>>
|
1. `"netbox_slm"` aus der `PLUGINS`-Liste entfernen.
|
||||||
```
|
2. Die Git-Zeile aus `/opt/netbox/local_requirements.txt` entfernen.
|
||||||
|
3. Das Python-Paket deinstallieren:
|
||||||
Now you can run commands from the netbox repository like this;
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ cd netbox/netbox
|
/opt/netbox/venv/bin/pip uninstall netbox-slm
|
||||||
$ 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
|
Die Plugin-Tabellen werden dabei nicht automatisch aus der Datenbank gelöscht.
|
||||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
|
||||||
|
## Ursprung und Lizenz
|
||||||
|
|
||||||
|
Dieses Repository basiert auf dem ursprünglichen Projekt
|
||||||
|
[ICTU/netbox_slm](https://github.com/ICTU/netbox_slm). **ICTU** ist der ursprüngliche Autor und Herausgeber.
|
||||||
|
[Mart Visser](https://github.com/MartVisser) wird im Originalprojekt als Ansprechpartner genannt.
|
||||||
|
|
||||||
|
Die Anpassungen in diesem Fork werden unter
|
||||||
|
[git.mrblake.cc/MrBlake/NetBox-SLM](https://git.mrblake.cc/MrBlake/NetBox-SLM) gepflegt.
|
||||||
|
|
||||||
|
Das Projekt steht unter der [Apache License 2.0](LICENSE). Die Hinweise auf die ursprünglichen Urheber bleiben
|
||||||
|
erhalten.
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Current status
|
|
||||||
|
|
||||||
Netbox SLM is a plugin for Netbox with mostly default configuration. We advise to monitor Netbox security findings and update accordingly.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Only the latest version of Netbox SLM is currently being supported with security updates. We aim to keep the plugin compatible with the latest Netbox version, but have no incentive to patch older tags at this point.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report security vulnerabilities by opening a ticket. The aim is to get back to you within 24 hours with a confirmation of the issue and a brief action plan or a request for more information.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
ARG PYTHON_VERSION=3.10
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS compile-image
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
RUN python -m venv /opt/netbox/venv
|
|
||||||
ENV PATH="/opt/netbox/venv/bin:$PATH"
|
|
||||||
|
|
||||||
COPY ../ /build
|
|
||||||
|
|
||||||
RUN pip install -U .[build]
|
|
||||||
RUN python -m build
|
|
||||||
RUN pip install --no-index /build
|
|
||||||
|
|
||||||
FROM netboxcommunity/netbox:v3.6.1
|
|
||||||
|
|
||||||
ARG PYTHON_VERSION=3.10
|
|
||||||
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM netboxcommunity/netbox:v3.6.1
|
|
||||||
|
|
||||||
RUN mkdir /ci && chmod go+w /ci
|
|
||||||
COPY ../ci/requirements_ci.txt /ci/
|
|
||||||
RUN pip install -r /ci/requirements_ci.txt
|
|
||||||
|
|
||||||
COPY ../netbox_slm /opt/netbox/netbox/netbox_slm
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
####
|
|
||||||
## We recommend to not edit this file.
|
|
||||||
## Create separate files to overwrite the settings.
|
|
||||||
## See `extra.py` as an example.
|
|
||||||
####
|
|
||||||
|
|
||||||
import re
|
|
||||||
from os import environ
|
|
||||||
from os.path import abspath, dirname, join
|
|
||||||
|
|
||||||
# For reference see https://netbox.readthedocs.io/en/stable/configuration/
|
|
||||||
# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
|
|
||||||
|
|
||||||
# Read secret from file
|
|
||||||
def _read_secret(secret_name, default = None):
|
|
||||||
try:
|
|
||||||
f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8')
|
|
||||||
except EnvironmentError:
|
|
||||||
return default
|
|
||||||
else:
|
|
||||||
with f:
|
|
||||||
return f.readline().strip()
|
|
||||||
|
|
||||||
_BASE_DIR = dirname(dirname(abspath(__file__)))
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# #
|
|
||||||
# Required settings #
|
|
||||||
# #
|
|
||||||
#########################
|
|
||||||
|
|
||||||
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
|
|
||||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
|
||||||
#
|
|
||||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
|
||||||
ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ')
|
|
||||||
|
|
||||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
|
||||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
|
||||||
DATABASE = {
|
|
||||||
'NAME': environ.get('DB_NAME', 'netbox'), # Database name
|
|
||||||
'USER': environ.get('DB_USER', ''), # PostgreSQL username
|
|
||||||
'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')),
|
|
||||||
# PostgreSQL password
|
|
||||||
'HOST': environ.get('DB_HOST', 'localhost'), # Database server
|
|
||||||
'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default)
|
|
||||||
'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')},
|
|
||||||
# Database connection SSLMODE
|
|
||||||
'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')),
|
|
||||||
# Max database connection age
|
|
||||||
'DISABLE_SERVER_SIDE_CURSORS': environ.get('DB_DISABLE_SERVER_SIDE_CURSORS', 'False').lower() == 'true',
|
|
||||||
# Disable the use of server-side cursors transaction pooling
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
|
||||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
|
||||||
# to use two separate database IDs.
|
|
||||||
REDIS = {
|
|
||||||
'tasks': {
|
|
||||||
'HOST': environ.get('REDIS_HOST', 'localhost'),
|
|
||||||
'PORT': int(environ.get('REDIS_PORT', 6379)),
|
|
||||||
'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')),
|
|
||||||
'DATABASE': int(environ.get('REDIS_DATABASE', 0)),
|
|
||||||
'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true',
|
|
||||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False').lower() == 'true',
|
|
||||||
},
|
|
||||||
'caching': {
|
|
||||||
'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')),
|
|
||||||
'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))),
|
|
||||||
'PASSWORD': _read_secret('redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', ''))),
|
|
||||||
'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)),
|
|
||||||
'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true',
|
|
||||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False')).lower() == 'true',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
|
||||||
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
|
||||||
# symbols. NetBox will not run without this defined. For more information, see
|
|
||||||
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
|
|
||||||
SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', ''))
|
|
||||||
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# #
|
|
||||||
# Optional settings #
|
|
||||||
# #
|
|
||||||
#########################
|
|
||||||
|
|
||||||
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
|
|
||||||
# application errors (assuming correct email settings are provided).
|
|
||||||
ADMINS = [
|
|
||||||
# ['John Doe', 'jdoe@example.com'],
|
|
||||||
]
|
|
||||||
|
|
||||||
# URL schemes that are allowed within links in NetBox
|
|
||||||
ALLOWED_URL_SCHEMES = (
|
|
||||||
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
|
||||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
|
||||||
BANNER_TOP = environ.get('BANNER_TOP', '')
|
|
||||||
BANNER_BOTTOM = environ.get('BANNER_BOTTOM', '')
|
|
||||||
|
|
||||||
# Text to include on the login page above the login form. HTML is allowed.
|
|
||||||
BANNER_LOGIN = environ.get('BANNER_LOGIN', '')
|
|
||||||
|
|
||||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
|
||||||
# BASE_PATH = 'netbox/'
|
|
||||||
BASE_PATH = environ.get('BASE_PATH', '')
|
|
||||||
|
|
||||||
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
|
|
||||||
CHANGELOG_RETENTION = int(environ.get('CHANGELOG_RETENTION', 90))
|
|
||||||
|
|
||||||
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
|
|
||||||
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
|
|
||||||
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
|
|
||||||
CORS_ORIGIN_ALLOW_ALL = environ.get('CORS_ORIGIN_ALLOW_ALL', 'False').lower() == 'true'
|
|
||||||
CORS_ORIGIN_WHITELIST = list(filter(None, environ.get('CORS_ORIGIN_WHITELIST', 'https://localhost').split(' ')))
|
|
||||||
CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in list(filter(None, environ.get('CORS_ORIGIN_REGEX_WHITELIST', '').split(' ')))]
|
|
||||||
|
|
||||||
# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag.
|
|
||||||
# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like:
|
|
||||||
# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev
|
|
||||||
CSRF_TRUSTED_ORIGINS = list(filter(None, environ.get('CSRF_TRUSTED_ORIGINS', '').split(' ')))
|
|
||||||
|
|
||||||
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
|
|
||||||
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
|
|
||||||
# on a production system.
|
|
||||||
DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
|
|
||||||
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# Email settings
|
|
||||||
EMAIL = {
|
|
||||||
'SERVER': environ.get('EMAIL_SERVER', 'localhost'),
|
|
||||||
'PORT': int(environ.get('EMAIL_PORT', 25)),
|
|
||||||
'USERNAME': environ.get('EMAIL_USERNAME', ''),
|
|
||||||
'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')),
|
|
||||||
'USE_SSL': environ.get('EMAIL_USE_SSL', 'False').lower() == 'true',
|
|
||||||
'USE_TLS': environ.get('EMAIL_USE_TLS', 'False').lower() == 'true',
|
|
||||||
'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''),
|
|
||||||
'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''),
|
|
||||||
'TIMEOUT': int(environ.get('EMAIL_TIMEOUT', 10)), # seconds
|
|
||||||
'FROM_EMAIL': environ.get('EMAIL_FROM', ''),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
|
|
||||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
|
||||||
ENFORCE_GLOBAL_UNIQUE = environ.get('ENFORCE_GLOBAL_UNIQUE', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
|
|
||||||
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
|
|
||||||
EXEMPT_VIEW_PERMISSIONS = list(filter(None, environ.get('EXEMPT_VIEW_PERMISSIONS', '').split(' ')))
|
|
||||||
|
|
||||||
# Enable GraphQL API.
|
|
||||||
GRAPHQL_ENABLED = environ.get('GRAPHQL_ENABLED', 'True').lower() == 'true'
|
|
||||||
|
|
||||||
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
|
|
||||||
# https://docs.djangoproject.com/en/stable/topics/logging/
|
|
||||||
LOGGING = {}
|
|
||||||
|
|
||||||
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
|
||||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
|
||||||
LOGIN_REQUIRED = environ.get('LOGIN_REQUIRED', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to
|
|
||||||
# re-authenticate. (Default: 1209600 [14 days])
|
|
||||||
LOGIN_TIMEOUT = int(environ.get('LOGIN_TIMEOUT', 1209600))
|
|
||||||
|
|
||||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
|
||||||
MAINTENANCE_MODE = environ.get('MAINTENANCE_MODE', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# Maps provider
|
|
||||||
MAPS_URL = environ.get('MAPS_URL', None)
|
|
||||||
|
|
||||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
|
||||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
|
||||||
# all objects by specifying "?limit=0".
|
|
||||||
MAX_PAGE_SIZE = int(environ.get('MAX_PAGE_SIZE', 1000))
|
|
||||||
|
|
||||||
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
|
|
||||||
# the default value of this setting is derived from the installed location.
|
|
||||||
MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media'))
|
|
||||||
|
|
||||||
# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
|
|
||||||
METRICS_ENABLED = environ.get('METRICS_ENABLED', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
|
|
||||||
NAPALM_USERNAME = environ.get('NAPALM_USERNAME', '')
|
|
||||||
NAPALM_PASSWORD = _read_secret('napalm_password', environ.get('NAPALM_PASSWORD', ''))
|
|
||||||
|
|
||||||
# NAPALM timeout (in seconds). (Default: 30)
|
|
||||||
NAPALM_TIMEOUT = int(environ.get('NAPALM_TIMEOUT', 30))
|
|
||||||
|
|
||||||
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
|
|
||||||
# be provided as a dictionary.
|
|
||||||
NAPALM_ARGS = {}
|
|
||||||
|
|
||||||
# Determine how many objects to display per page within a list. (Default: 50)
|
|
||||||
PAGINATE_COUNT = int(environ.get('PAGINATE_COUNT', 50))
|
|
||||||
|
|
||||||
# Enable installed plugins. Add the name of each plugin to the list.
|
|
||||||
PLUGINS = ['netbox_slm']
|
|
||||||
|
|
||||||
# Plugins configuration settings. These settings are used by various plugins that the user may have installed.
|
|
||||||
# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
|
|
||||||
PLUGINS_CONFIG = {
|
|
||||||
}
|
|
||||||
|
|
||||||
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
|
||||||
# prefer IPv4 instead.
|
|
||||||
PREFER_IPV4 = environ.get('PREFER_IPV4', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
|
|
||||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22))
|
|
||||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220))
|
|
||||||
|
|
||||||
# Remote authentication support
|
|
||||||
REMOTE_AUTH_ENABLED = environ.get('REMOTE_AUTH_ENABLED', 'False').lower() == 'true'
|
|
||||||
REMOTE_AUTH_BACKEND = environ.get('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
|
||||||
REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
|
|
||||||
REMOTE_AUTH_AUTO_CREATE_USER = environ.get('REMOTE_AUTH_AUTO_CREATE_USER', 'True').lower() == 'true'
|
|
||||||
REMOTE_AUTH_DEFAULT_GROUPS = list(filter(None, environ.get('REMOTE_AUTH_DEFAULT_GROUPS', '').split(' ')))
|
|
||||||
|
|
||||||
# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the
|
|
||||||
# version check or use the URL below to check for release in the official NetBox repository.
|
|
||||||
# https://api.github.com/repos/netbox-community/netbox/releases
|
|
||||||
RELEASE_CHECK_URL = environ.get('RELEASE_CHECK_URL', None)
|
|
||||||
|
|
||||||
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
|
||||||
# this setting is derived from the installed location.
|
|
||||||
REPORTS_ROOT = environ.get('REPORTS_ROOT', '/etc/netbox/reports')
|
|
||||||
|
|
||||||
# Maximum execution time for background tasks, in seconds.
|
|
||||||
RQ_DEFAULT_TIMEOUT = int(environ.get('RQ_DEFAULT_TIMEOUT', 300))
|
|
||||||
|
|
||||||
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
|
||||||
# this setting is derived from the installed location.
|
|
||||||
SCRIPTS_ROOT = environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts')
|
|
||||||
|
|
||||||
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
|
|
||||||
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
|
|
||||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
|
||||||
SESSION_FILE_PATH = environ.get('SESSIONS_ROOT', None)
|
|
||||||
|
|
||||||
# Time zone (default: UTC)
|
|
||||||
TIME_ZONE = environ.get('TIME_ZONE', 'UTC')
|
|
||||||
|
|
||||||
# Date/time formatting. See the following link for supported formats:
|
|
||||||
# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
|
|
||||||
DATE_FORMAT = environ.get('DATE_FORMAT', 'N j, Y')
|
|
||||||
SHORT_DATE_FORMAT = environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
|
|
||||||
TIME_FORMAT = environ.get('TIME_FORMAT', 'g:i a')
|
|
||||||
SHORT_TIME_FORMAT = environ.get('SHORT_TIME_FORMAT', 'H:i:s')
|
|
||||||
DATETIME_FORMAT = environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
|
|
||||||
SHORT_DATETIME_FORMAT = environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
PLUGINS = ['netbox_slm']
|
|
||||||
DEBUG = True
|
|
||||||
SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
|
|
||||||
DEVELOPER = True
|
|
||||||
PLUGINS_CONFIG = {
|
|
||||||
'netbox_slm': dict(
|
|
||||||
TEST_RUNNER='xmlrunner.extra.djangotestrunner.XMLTestRunner',
|
|
||||||
TEST_OUTPUT_DIR='/ci/reports/',
|
|
||||||
TEST_OUTPUT_FILE_NAME='junit.xml',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
netbox:
|
|
||||||
ports:
|
|
||||||
- "8001:8080"
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: ci/Dockerfile-CI
|
|
||||||
image: netbox:slm
|
|
||||||
environment:
|
|
||||||
- COVERAGE_FILE=/ci/.coverage
|
|
||||||
volumes:
|
|
||||||
- ./reports:/ci/reports
|
|
||||||
netbox-worker:
|
|
||||||
image: netbox:slm
|
|
||||||
netbox-housekeeping:
|
|
||||||
image: netbox:slm
|
|
||||||
postgres:
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
redis:
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
netbox: &netbox
|
|
||||||
image: netboxcommunity/netbox:${VERSION-v3.6.1}
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
- redis-cache
|
|
||||||
- netbox-worker
|
|
||||||
env_file: env/netbox.env
|
|
||||||
user: 'unit:root'
|
|
||||||
volumes:
|
|
||||||
- ./configuration:/etc/netbox/config:z,ro
|
|
||||||
- netbox-media-files:/opt/netbox/netbox/media:z
|
|
||||||
netbox-worker:
|
|
||||||
<<: *netbox
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- postgres
|
|
||||||
command:
|
|
||||||
- /opt/netbox/venv/bin/python
|
|
||||||
- /opt/netbox/netbox/manage.py
|
|
||||||
- rqworker
|
|
||||||
netbox-housekeeping:
|
|
||||||
<<: *netbox
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- postgres
|
|
||||||
command:
|
|
||||||
- /opt/netbox/housekeeping.sh
|
|
||||||
|
|
||||||
# postgres
|
|
||||||
postgres:
|
|
||||||
image: postgres:14-alpine
|
|
||||||
env_file: env/postgres.env
|
|
||||||
volumes:
|
|
||||||
- netbox-postgres-data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
# redis
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
|
||||||
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
|
||||||
env_file: env/redis.env
|
|
||||||
volumes:
|
|
||||||
- netbox-redis-data:/data
|
|
||||||
redis-cache:
|
|
||||||
image: redis:7-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
|
||||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
|
||||||
env_file: env/redis-cache.env
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
netbox-media-files:
|
|
||||||
driver: local
|
|
||||||
netbox-postgres-data:
|
|
||||||
driver: local
|
|
||||||
netbox-redis-data:
|
|
||||||
driver: local
|
|
||||||
Vendored
-42
@@ -1,42 +0,0 @@
|
|||||||
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
@@ -1,3 +0,0 @@
|
|||||||
POSTGRES_DB=netbox
|
|
||||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
|
||||||
POSTGRES_USER=netbox
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
REDIS_PASSWORD=H733Kdjndks81
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
coverage==7.2.2
|
|
||||||
unittest-xml-reporting==3.2.0
|
|
||||||
+37
-5
@@ -1,18 +1,50 @@
|
|||||||
from extras.plugins import PluginConfig
|
"""
|
||||||
|
Copyright 2022-2025 ICTU
|
||||||
|
|
||||||
__version__ = "1.5"
|
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
|
||||||
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
|
||||||
|
__version__ = "1.13.0"
|
||||||
|
|
||||||
|
|
||||||
class SLMConfig(PluginConfig):
|
class SLMConfig(PluginConfig):
|
||||||
name = "netbox_slm"
|
name = "netbox_slm"
|
||||||
verbose_name = "Software Lifecycle Management"
|
verbose_name = _("Licences")
|
||||||
description = "Software Lifecycle Management Netbox Plugin."
|
|
||||||
version = __version__
|
version = __version__
|
||||||
|
description = _("Licence Management NetBox Plugin.")
|
||||||
author = "ICTU"
|
author = "ICTU"
|
||||||
author_email = "open-source-projects@ictu.nl"
|
author_email = "open-source-projects@ictu.nl"
|
||||||
base_url = "slm"
|
base_url = "slm"
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {"version_info": False}
|
default_settings = {
|
||||||
|
"top_level_menu": True,
|
||||||
|
"link_cluster_installations": "right",
|
||||||
|
"link_device_installations": "right",
|
||||||
|
"link_virtualmachine_installations": "right",
|
||||||
|
}
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
|
||||||
|
from netbox.events import EVENT_TYPE_KIND_WARNING, EventType
|
||||||
|
from netbox_slm.events import LICENSE_EXPIRING
|
||||||
|
|
||||||
|
EventType(LICENSE_EXPIRING, gettext("License expiring"), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||||
|
|
||||||
|
from netbox_slm import jobs # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
config = SLMConfig
|
config = SLMConfig
|
||||||
|
|||||||
+128
-44
@@ -1,61 +1,140 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
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",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"license_file",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"default_notification_intervals",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"email_notifications",
|
||||||
|
"software_product",
|
||||||
|
"version",
|
||||||
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
|
"tags",
|
||||||
|
"comments",
|
||||||
|
"custom_field_data",
|
||||||
|
"created",
|
||||||
|
"last_updated",
|
||||||
|
)
|
||||||
|
brief_fields = ("id", "display", "url", "name", "description")
|
||||||
|
|
||||||
|
def get_display(self, obj):
|
||||||
|
return f"{obj}"
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
request = self.context.get("request")
|
||||||
|
if (
|
||||||
|
"email_notifications" in attrs
|
||||||
|
and request is not None
|
||||||
|
and not request.user.has_perm("netbox_slm.manage_softwarelicense_email_notifications")
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"email_notifications": _("You do not have permission to change email notifications.")}
|
||||||
|
)
|
||||||
|
|
||||||
|
tenant_group = attrs.get("tenant_group", getattr(self.instance, "tenant_group", None))
|
||||||
|
tenant = attrs.get("tenant", getattr(self.instance, "tenant", None))
|
||||||
|
if tenant_group and tenant and tenant.group_id != tenant_group.pk:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"tenant": _("The selected tenant does not belong to the selected tenant group.")}
|
||||||
|
)
|
||||||
|
lifetime = attrs.get("lifetime", getattr(self.instance, "lifetime", False))
|
||||||
|
renewal_interval = attrs.get("renewal_interval", getattr(self.instance, "renewal_interval", None))
|
||||||
|
renewal_interval_unit = attrs.get(
|
||||||
|
"renewal_interval_unit", getattr(self.instance, "renewal_interval_unit", "")
|
||||||
|
)
|
||||||
|
expiration_date = attrs.get("expiration_date", getattr(self.instance, "expiration_date", None))
|
||||||
|
if bool(renewal_interval) != bool(renewal_interval_unit):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"renewal_interval": _("Renewal interval and unit must be specified together.")}
|
||||||
|
)
|
||||||
|
if lifetime and (renewal_interval or renewal_interval_unit or expiration_date):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")}
|
||||||
|
)
|
||||||
|
custom_interval = attrs.get(
|
||||||
|
"custom_notification_interval", getattr(self.instance, "custom_notification_interval", None)
|
||||||
|
)
|
||||||
|
custom_interval_unit = attrs.get(
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
getattr(self.instance, "custom_notification_interval_unit", ""),
|
||||||
|
)
|
||||||
|
if bool(custom_interval) != bool(custom_interval_unit):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{
|
||||||
|
"custom_notification_interval": _(
|
||||||
|
"Custom notification interval and unit must be specified together."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||||
display = serializers.SerializerMethodField()
|
display = SerializerMethodField()
|
||||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProduct
|
model = SoftwareProduct
|
||||||
fields = [
|
fields = (
|
||||||
"id",
|
"id",
|
||||||
"display",
|
"display",
|
||||||
"url",
|
"url",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
|
"manufacturer",
|
||||||
|
"description",
|
||||||
"tags",
|
"tags",
|
||||||
|
"comments",
|
||||||
"custom_field_data",
|
"custom_field_data",
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
"last_updated",
|
||||||
]
|
)
|
||||||
|
brief_fields = ("id", "display", "url", "name", "description")
|
||||||
def get_display(self, obj):
|
|
||||||
return f"{obj.manufacturer} - {obj}"
|
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
|
||||||
display = serializers.SerializerMethodField()
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SoftwareProductVersion
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"display",
|
|
||||||
"url",
|
|
||||||
"name",
|
|
||||||
"software_product",
|
|
||||||
"tags",
|
|
||||||
"custom_field_data",
|
|
||||||
"created",
|
|
||||||
"last_updated",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
return f"{obj}"
|
return f"{obj}"
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||||
display = serializers.SerializerMethodField()
|
display = SerializerMethodField()
|
||||||
url = serializers.HyperlinkedIdentityField(
|
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail")
|
||||||
view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fields = [
|
fields = (
|
||||||
"id",
|
"id",
|
||||||
"display",
|
"display",
|
||||||
"url",
|
"url",
|
||||||
@@ -65,39 +144,44 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
|||||||
"software_product",
|
"software_product",
|
||||||
"version",
|
"version",
|
||||||
"tags",
|
"tags",
|
||||||
|
"comments",
|
||||||
"custom_field_data",
|
"custom_field_data",
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
"last_updated",
|
||||||
]
|
)
|
||||||
|
brief_fields = ("id", "display", "url")
|
||||||
|
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
return f"{obj}"
|
return f"{obj}"
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||||
display = serializers.SerializerMethodField()
|
display = SerializerMethodField()
|
||||||
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
|
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareLicense
|
model = SoftwareProductVersion
|
||||||
fields = [
|
fields = (
|
||||||
"id",
|
"id",
|
||||||
"display",
|
"display",
|
||||||
"url",
|
"url",
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"type",
|
"release_date",
|
||||||
"stored_location",
|
"documentation_url",
|
||||||
"start_date",
|
"end_of_support",
|
||||||
"expiration_date",
|
"filename",
|
||||||
|
"file_checksum",
|
||||||
|
"file_link",
|
||||||
|
"release_type",
|
||||||
"software_product",
|
"software_product",
|
||||||
"version",
|
|
||||||
"installation",
|
|
||||||
"tags",
|
"tags",
|
||||||
|
"comments",
|
||||||
"custom_field_data",
|
"custom_field_data",
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
"last_updated",
|
||||||
]
|
)
|
||||||
|
brief_fields = ("id", "display", "url", "name", "description")
|
||||||
|
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
return f"{obj}"
|
return f"{obj}"
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ router.register("softwareproducts", SoftwareProductViewSet)
|
|||||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|||||||
+10
-9
@@ -13,37 +13,38 @@ from netbox_slm.filtersets import (
|
|||||||
SoftwareProductInstallationFilterSet,
|
SoftwareProductInstallationFilterSet,
|
||||||
SoftwareLicenseFilterSet,
|
SoftwareLicenseFilterSet,
|
||||||
)
|
)
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
from netbox_slm.models import (
|
||||||
|
SoftwareProduct,
|
||||||
|
SoftwareProductVersion,
|
||||||
|
SoftwareProductInstallation,
|
||||||
|
SoftwareLicense,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NetboxSLMRootView(APIRootView):
|
class NetboxSLMRootView(APIRootView):
|
||||||
"""
|
|
||||||
NetboxSLM API root view
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "NetboxSLM"
|
return "NetboxSLM"
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductViewSet(NetBoxModelViewSet):
|
class SoftwareProductViewSet(NetBoxModelViewSet):
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all().order_by("id")
|
||||||
serializer_class = SoftwareProductSerializer
|
serializer_class = SoftwareProductSerializer
|
||||||
filterset_class = SoftwareProductFilterSet
|
filterset_class = SoftwareProductFilterSet
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all().order_by("id")
|
||||||
serializer_class = SoftwareProductVersionSerializer
|
serializer_class = SoftwareProductVersionSerializer
|
||||||
filterset_class = SoftwareProductVersionFilterSet
|
filterset_class = SoftwareProductVersionFilterSet
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all().order_by("id")
|
||||||
serializer_class = SoftwareProductInstallationSerializer
|
serializer_class = SoftwareProductInstallationSerializer
|
||||||
filterset_class = SoftwareProductInstallationFilterSet
|
filterset_class = SoftwareProductInstallationFilterSet
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all().order_by("id")
|
||||||
serializer_class = SoftwareLicenseSerializer
|
serializer_class = SoftwareLicenseSerializer
|
||||||
filterset_class = SoftwareLicenseFilterSet
|
filterset_class = SoftwareLicenseFilterSet
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
LICENSE_EXPIRING = "netbox_slm_license_expiring"
|
||||||
@@ -1,12 +1,29 @@
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter, NumberFilter
|
||||||
|
|
||||||
|
from dcim.models import Device, Manufacturer
|
||||||
from netbox.filtersets import NetBoxModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
from netbox_slm.models import (
|
||||||
|
SoftwareProduct,
|
||||||
|
SoftwareProductVersion,
|
||||||
|
SoftwareProductInstallation,
|
||||||
|
SoftwareLicense,
|
||||||
|
SoftwareReleaseTypes,
|
||||||
|
RenewalIntervalUnits,
|
||||||
|
)
|
||||||
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||||
"""Filter capabilities for SoftwareProduct instances."""
|
"""Filter capabilities for SoftwareProduct instances."""
|
||||||
|
|
||||||
|
name = CharFilter(lookup_expr="icontains")
|
||||||
|
description = CharFilter(lookup_expr="icontains")
|
||||||
|
|
||||||
|
manufacturer_id = ModelMultipleChoiceFilter(queryset=Manufacturer.objects.all())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProduct
|
model = SoftwareProduct
|
||||||
fields = tuple()
|
fields = tuple()
|
||||||
@@ -15,16 +32,37 @@ class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
|||||||
"""Perform the filtered search."""
|
"""Perform the filtered search."""
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(name__icontains=value) | Q(manufacturer__name__icontains=value)
|
qs_filter = (
|
||||||
|
Q(name__icontains=value)
|
||||||
|
| Q(description__icontains=value)
|
||||||
|
| Q(manufacturer__name__icontains=value)
|
||||||
|
| Q(comments__icontains=value)
|
||||||
|
)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||||
"""Filter capabilities for SoftwareProductVersion instances."""
|
"""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:
|
class Meta:
|
||||||
model = SoftwareProductVersion
|
model = SoftwareProductVersion
|
||||||
fields = ("software_product",)
|
fields = tuple()
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
"""Perform the filtered search."""
|
"""Perform the filtered search."""
|
||||||
@@ -32,8 +70,10 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
qs_filter = (
|
qs_filter = (
|
||||||
Q(name__icontains=value)
|
Q(name__icontains=value)
|
||||||
|
| Q(description__icontains=value)
|
||||||
| Q(software_product__name__icontains=value)
|
| Q(software_product__name__icontains=value)
|
||||||
| Q(software_product__manufacturer__name__icontains=value)
|
| Q(software_product__manufacturer__name__icontains=value)
|
||||||
|
| Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
@@ -41,9 +81,21 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
|||||||
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||||
"""Filter capabilities for SoftwareProductInstallation instances."""
|
"""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:
|
class Meta:
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fields = ("software_product",)
|
fields = tuple()
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
"""Perform the filtered search."""
|
"""Perform the filtered search."""
|
||||||
@@ -53,6 +105,7 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
|||||||
Q(software_product__name__icontains=value)
|
Q(software_product__name__icontains=value)
|
||||||
| Q(software_product__manufacturer__name__icontains=value)
|
| Q(software_product__manufacturer__name__icontains=value)
|
||||||
| Q(version__name__icontains=value)
|
| Q(version__name__icontains=value)
|
||||||
|
| Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
@@ -60,9 +113,26 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
|||||||
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||||
"""Filter capabilities for SoftwareLicense instances."""
|
"""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")
|
||||||
|
renewal_interval = NumberFilter()
|
||||||
|
renewal_interval_unit = MultipleChoiceFilter(choices=RenewalIntervalUnits.choices)
|
||||||
|
|
||||||
|
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())
|
||||||
|
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label=_("Tenant Group"))
|
||||||
|
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label=_("Tenant"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareLicense
|
model = SoftwareLicense
|
||||||
fields = tuple()
|
fields = ("support", "lifetime", "expiration_notifications_enabled", "email_notifications")
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
"""Perform the filtered search."""
|
"""Perform the filtered search."""
|
||||||
@@ -70,10 +140,14 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
qs_filter = (
|
qs_filter = (
|
||||||
Q(name__icontains=value)
|
Q(name__icontains=value)
|
||||||
|
| Q(description__icontains=value)
|
||||||
| Q(software_product__name__icontains=value)
|
| Q(software_product__name__icontains=value)
|
||||||
| Q(version__name__icontains=value)
|
| Q(version__name__icontains=value)
|
||||||
| Q(installation__device__name__icontains=value)
|
| Q(installation__device__name__icontains=value)
|
||||||
| Q(installation__virtualmachine__name__icontains=value)
|
| Q(installation__virtualmachine__name__icontains=value)
|
||||||
| Q(installation__cluster__name__icontains=value)
|
| Q(installation__cluster__name__icontains=value)
|
||||||
|
| Q(tenant_group__name__icontains=value)
|
||||||
|
| Q(tenant__name__icontains=value)
|
||||||
|
| Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
from .software_license import (
|
from .software_license import (
|
||||||
SoftwareLicenseForm,
|
SoftwareLicenseForm,
|
||||||
SoftwareLicenseFilterForm,
|
SoftwareLicenseFilterForm,
|
||||||
SoftwareLicenseImportForm,
|
SoftwareLicenseBulkImportForm,
|
||||||
SoftwareLicenseBulkEditForm,
|
SoftwareLicenseBulkEditForm,
|
||||||
)
|
)
|
||||||
from .software_product import (
|
from .software_product import (
|
||||||
SoftwareProductForm,
|
SoftwareProductForm,
|
||||||
SoftwareProductFilterForm,
|
SoftwareProductFilterForm,
|
||||||
SoftwareProductImportForm,
|
SoftwareProductBulkImportForm,
|
||||||
SoftwareProductBulkEditForm,
|
SoftwareProductBulkEditForm,
|
||||||
)
|
)
|
||||||
from .software_product_installation import (
|
from .software_product_installation import (
|
||||||
SoftwareProductInstallationForm,
|
SoftwareProductInstallationForm,
|
||||||
SoftwareProductInstallationFilterForm,
|
SoftwareProductInstallationFilterForm,
|
||||||
SoftwareProductInstallationImportForm,
|
SoftwareProductInstallationBulkImportForm,
|
||||||
SoftwareProductInstallationBulkEditForm,
|
SoftwareProductInstallationBulkEditForm,
|
||||||
)
|
)
|
||||||
from .software_product_version import (
|
from .software_product_version import (
|
||||||
SoftwareProductVersionForm,
|
SoftwareProductVersionForm,
|
||||||
SoftwareProductVersionFilterForm,
|
SoftwareProductVersionFilterForm,
|
||||||
SoftwareProductVersionImportForm,
|
SoftwareProductVersionBulkImportForm,
|
||||||
SoftwareProductVersionBulkEditForm,
|
SoftwareProductVersionBulkEditForm,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,30 +1,87 @@
|
|||||||
from django.forms import DateField
|
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField, Textarea
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
from netbox_slm.models import (
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField, LaxURLField
|
SoftwareProduct,
|
||||||
|
SoftwareProductVersion,
|
||||||
|
SoftwareProductInstallation,
|
||||||
|
SoftwareLicense,
|
||||||
|
RenewalIntervalUnits,
|
||||||
|
NotificationIntervalUnits,
|
||||||
|
spdx_license_names,
|
||||||
|
)
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
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
|
from utilities.forms.widgets import APISelect, DatePicker
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseForm(NetBoxModelForm):
|
class SoftwareLicenseForm(NetBoxModelForm):
|
||||||
"""Form for creating a new SoftwareLicense object."""
|
comments = CommentField()
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet(
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"software_product",
|
||||||
|
"version",
|
||||||
|
"installation",
|
||||||
|
name=_("Software License"),
|
||||||
|
),
|
||||||
|
FieldSet("tenant_group", "tenant", name=_("Tenancy")),
|
||||||
|
FieldSet(
|
||||||
|
"type",
|
||||||
|
"spdx_expression",
|
||||||
|
"stored_location",
|
||||||
|
"stored_location_url",
|
||||||
|
"start_date",
|
||||||
|
"expiration_date",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
name=_("License Details"),
|
||||||
|
),
|
||||||
|
FieldSet("license_file", "license_key", "provider_portal_url", name=_("License Credentials")),
|
||||||
|
FieldSet(
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"default_notification_intervals",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"email_notifications",
|
||||||
|
name=_("Expiration Notifications"),
|
||||||
|
),
|
||||||
|
FieldSet("tags", name=_("Tags")),
|
||||||
|
)
|
||||||
|
|
||||||
|
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label=_("SPDX expression"))
|
||||||
stored_location_url = LaxURLField(required=False)
|
stored_location_url = LaxURLField(required=False)
|
||||||
|
start_date = DateField(required=False, widget=DatePicker())
|
||||||
|
expiration_date = DateField(required=False, widget=DatePicker())
|
||||||
|
renewal_interval = IntegerField(required=False, min_value=1)
|
||||||
|
license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3}))
|
||||||
|
provider_portal_url = LaxURLField(required=False)
|
||||||
|
custom_notification_interval = IntegerField(required=False, min_value=1)
|
||||||
|
|
||||||
software_product = DynamicModelChoiceField(
|
software_product = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProduct.objects.all(),
|
queryset=SoftwareProduct.objects.all(),
|
||||||
required=True,
|
required=True,
|
||||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
label=_("Software Product"),
|
||||||
)
|
)
|
||||||
version = DynamicModelChoiceField(
|
version = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProductVersion.objects.all(),
|
queryset=SoftwareProductVersion.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||||
query_params={
|
query_params=dict(software_product="$software_product"),
|
||||||
"software_product": "$software_product",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
installation = DynamicModelChoiceField(
|
installation = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProductInstallation.objects.all(),
|
queryset=SoftwareProductInstallation.objects.all(),
|
||||||
@@ -32,71 +89,256 @@ class SoftwareLicenseForm(NetBoxModelForm):
|
|||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
||||||
),
|
),
|
||||||
query_params={
|
query_params=dict(software_product="$software_product"),
|
||||||
"software_product": "$software_product",
|
)
|
||||||
},
|
tenant_group = DynamicModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Tenant Group"),
|
||||||
|
)
|
||||||
|
tenant = DynamicModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={"group_id": "$tenant_group"},
|
||||||
|
label=_("Tenant"),
|
||||||
)
|
)
|
||||||
start_date = DateField(label=_("Start date"), required=False, widget=DatePicker())
|
|
||||||
expiration_date = DateField(label=_("Expiration date"), required=False, widget=DatePicker())
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareLicense
|
model = SoftwareLicense
|
||||||
fields = (
|
fields = (
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
|
"software_product",
|
||||||
"type",
|
"type",
|
||||||
|
"spdx_expression",
|
||||||
"stored_location",
|
"stored_location",
|
||||||
"stored_location_url",
|
"stored_location_url",
|
||||||
"start_date",
|
"start_date",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
"software_product",
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"license_file",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"default_notification_intervals",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"email_notifications",
|
||||||
"version",
|
"version",
|
||||||
"installation",
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
"tags",
|
"tags",
|
||||||
|
"comments",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = SoftwareLicense
|
model = SoftwareLicense
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
FieldSet("q", "filter_id", "tag"),
|
||||||
None,
|
FieldSet(
|
||||||
(
|
"name",
|
||||||
"q",
|
"description",
|
||||||
"tag",
|
"type",
|
||||||
),
|
"spdx_expression",
|
||||||
|
"stored_location",
|
||||||
|
"support",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"email_notifications",
|
||||||
|
"software_product_id",
|
||||||
|
"version_id",
|
||||||
|
"installation_id",
|
||||||
|
"tenant_group_id",
|
||||||
|
"tenant_id",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
selector_fields = ("q", "filter_id", "name")
|
||||||
|
|
||||||
tag = TagFilterField(model)
|
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)
|
||||||
|
lifetime = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
renewal_interval = IntegerField(required=False, min_value=1)
|
||||||
|
renewal_interval_unit = ChoiceField(required=False, choices=RenewalIntervalUnits.choices)
|
||||||
|
expiration_notifications_enabled = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
email_notifications = 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"),
|
||||||
|
)
|
||||||
|
tenant_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Tenant Group"),
|
||||||
|
)
|
||||||
|
tenant_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={"group_id": "$tenant_group_id"},
|
||||||
|
label=_("Tenant"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
|
||||||
|
support = NullBooleanField(
|
||||||
|
required=False, help_text=_("Support (values other than 'True' and 'False' are ignored)")
|
||||||
|
)
|
||||||
|
|
||||||
class SoftwareLicenseImportForm(NetBoxModelImportForm):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareLicense
|
model = SoftwareLicense
|
||||||
fields = (
|
fields = (
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
|
"software_product",
|
||||||
"type",
|
"type",
|
||||||
|
"spdx_expression",
|
||||||
"stored_location",
|
"stored_location",
|
||||||
"stored_location_url",
|
|
||||||
"start_date",
|
"start_date",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"default_notification_intervals",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"email_notifications",
|
||||||
|
"version",
|
||||||
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
|
"tags",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
|
|
||||||
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
|
|
||||||
installation = DynamicModelChoiceField(queryset=SoftwareProductInstallation.objects.all(), required=False)
|
|
||||||
model = SoftwareLicense
|
model = SoftwareLicense
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
FieldSet(
|
||||||
None,
|
"type",
|
||||||
(
|
"spdx_expression",
|
||||||
"software_product",
|
"stored_location",
|
||||||
"version",
|
"stored_location_url",
|
||||||
"installation",
|
"start_date",
|
||||||
),
|
"expiration_date",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"default_notification_intervals",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"email_notifications",
|
||||||
|
"software_product",
|
||||||
|
"version",
|
||||||
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
nullable_fields = (
|
||||||
|
"type",
|
||||||
|
"spdx_expression",
|
||||||
|
"stored_location",
|
||||||
|
"stored_location_url",
|
||||||
|
"start_date",
|
||||||
|
"expiration_date",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
"custom_notification_interval",
|
||||||
|
"custom_notification_interval_unit",
|
||||||
|
"version",
|
||||||
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
lifetime = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
renewal_interval = IntegerField(required=False, min_value=1)
|
||||||
|
renewal_interval_unit = ChoiceField(required=False, choices=RenewalIntervalUnits.choices)
|
||||||
|
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
license_amount = IntegerField(required=False, min_value=0)
|
||||||
|
license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3}))
|
||||||
|
provider_portal_url = LaxURLField(required=False)
|
||||||
|
expiration_notifications_enabled = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
default_notification_intervals = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
custom_notification_interval = IntegerField(required=False, min_value=1)
|
||||||
|
custom_notification_interval_unit = ChoiceField(required=False, choices=NotificationIntervalUnits.choices)
|
||||||
|
email_notifications = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
tenant_group = DynamicModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Tenant Group"),
|
||||||
|
)
|
||||||
|
tenant = DynamicModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={"group_id": "$tenant_group"},
|
||||||
|
label=_("Tenant"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
from django import forms
|
from django.forms import CharField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Manufacturer
|
from dcim.models import Manufacturer
|
||||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||||
from netbox_slm.models import SoftwareProduct
|
from netbox_slm.models import SoftwareProduct
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
from utilities.forms.fields import (
|
||||||
|
CommentField,
|
||||||
|
DynamicModelChoiceField,
|
||||||
|
TagFilterField,
|
||||||
|
DynamicModelMultipleChoiceField,
|
||||||
|
)
|
||||||
|
from utilities.forms.rendering import FieldSet
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductForm(NetBoxModelForm):
|
class SoftwareProductForm(NetBoxModelForm):
|
||||||
"""Form for creating a new SoftwareProduct object."""
|
comments = CommentField()
|
||||||
|
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@@ -16,31 +23,54 @@ class SoftwareProductForm(NetBoxModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProduct
|
model = SoftwareProduct
|
||||||
fields = ("name", "manufacturer", "description", "tags")
|
fields = (
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"manufacturer",
|
||||||
|
"tags",
|
||||||
|
"comments",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = SoftwareProduct
|
model = SoftwareProduct
|
||||||
fieldsets = ((None, ("q", "tag")),)
|
fieldsets = (
|
||||||
|
FieldSet("q", "filter_id", "tag"),
|
||||||
|
FieldSet("name", "description", "manufacturer_id"),
|
||||||
|
)
|
||||||
|
selector_fields = ("q", "filter_id", "name")
|
||||||
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
name = CharField(required=False)
|
||||||
|
description = CharField(required=False)
|
||||||
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Manufacturer"),
|
||||||
|
)
|
||||||
|
|
||||||
class SoftwareProductImportForm(NetBoxModelImportForm):
|
|
||||||
|
class SoftwareProductBulkImportForm(NetBoxModelImportForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProduct
|
model = SoftwareProduct
|
||||||
fields = (
|
fields = (
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
|
"tags",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
model = SoftwareProduct
|
||||||
queryset=SoftwareProduct.objects.all(),
|
fieldsets = (FieldSet("manufacturer"),)
|
||||||
widget=forms.MultipleHiddenInput(),
|
nullable_fields = ("manufacturer", "comments")
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
tag = TagFilterField(model)
|
||||||
model = SoftwareProduct
|
comments = CommentField()
|
||||||
nullable_fields = []
|
|
||||||
|
manufacturer = DynamicModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,83 +1,164 @@
|
|||||||
from django import forms
|
from django.forms import ValidationError
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||||
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
from utilities.forms.fields import (
|
||||||
|
CommentField,
|
||||||
|
DynamicModelChoiceField,
|
||||||
|
TagFilterField,
|
||||||
|
DynamicModelMultipleChoiceField,
|
||||||
|
)
|
||||||
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import APISelect
|
from utilities.forms.widgets import APISelect
|
||||||
from virtualization.models import VirtualMachine, Cluster
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||||
"""Form for creating a new SoftwareProductInstallation object."""
|
comments = CommentField()
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet("device", "virtualmachine", "cluster", name=_("Platform Destination (select exactly one)")),
|
||||||
|
FieldSet("software_product", "version", name=_("Software")),
|
||||||
|
FieldSet("tags", name=_("Tags")),
|
||||||
|
)
|
||||||
|
|
||||||
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
device = DynamicModelChoiceField(
|
||||||
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
|
queryset=Device.objects.all(),
|
||||||
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
required=False,
|
||||||
|
help_text=_("Select either a device, a virtual machine, or a cluster."),
|
||||||
|
)
|
||||||
|
virtualmachine = DynamicModelChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Virtual Machine"),
|
||||||
|
help_text=_("Leave empty when another platform destination is selected."),
|
||||||
|
)
|
||||||
|
cluster = DynamicModelChoiceField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
|
help_text=_("Leave empty when another platform destination is selected."),
|
||||||
|
)
|
||||||
software_product = DynamicModelChoiceField(
|
software_product = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProduct.objects.all(),
|
queryset=SoftwareProduct.objects.all(),
|
||||||
required=True,
|
required=True,
|
||||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
label=_("Software Product"),
|
||||||
)
|
)
|
||||||
version = DynamicModelChoiceField(
|
version = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProductVersion.objects.all(),
|
queryset=SoftwareProductVersion.objects.all(),
|
||||||
required=True,
|
required=True,
|
||||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||||
query_params={
|
query_params=dict(software_product="$software_product"),
|
||||||
"software_product": "$software_product",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fields = ("device", "virtualmachine", "cluster", "software_product", "version", "tags")
|
fields = (
|
||||||
|
"device",
|
||||||
|
"virtualmachine",
|
||||||
|
"cluster",
|
||||||
|
"software_product",
|
||||||
|
"version",
|
||||||
|
"tags",
|
||||||
|
"comments",
|
||||||
|
)
|
||||||
|
|
||||||
def clean_version(self):
|
def clean_version(self):
|
||||||
version = self.cleaned_data["version"]
|
version = self.cleaned_data["version"]
|
||||||
software_product = self.cleaned_data["software_product"]
|
software_product = self.cleaned_data["software_product"]
|
||||||
if version not in software_product.softwareproductversion_set.all():
|
if version not in software_product.softwareproductversion_set.all():
|
||||||
raise forms.ValidationError(
|
raise ValidationError(
|
||||||
_(
|
_("Version '%(version)s' does not belong to %(product)s. Select a compatible version.")
|
||||||
f"Version `{version}` doesn't exist on {software_product}, make sure you've "
|
% {"version": version, "product": software_product}
|
||||||
f"selected a compatible version or first select the software product."
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
destinations = [
|
||||||
|
self.cleaned_data.get("device"),
|
||||||
|
self.cleaned_data.get("virtualmachine"),
|
||||||
|
self.cleaned_data.get("cluster"),
|
||||||
|
]
|
||||||
|
if sum(destination is not None for destination in destinations) != 1:
|
||||||
|
raise ValidationError(_("Select exactly one platform destination: device, virtual machine, or cluster."))
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
FieldSet("q", "filter_id", "tag"),
|
||||||
None,
|
FieldSet("device_id", "virtualmachine_id", "cluster_id", "software_product_id", "version_id"),
|
||||||
(
|
|
||||||
"q",
|
|
||||||
"tag",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
selector_fields = ("filter_id", "q")
|
||||||
|
|
||||||
tag = TagFilterField(model)
|
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 SoftwareProductInstallationImportForm(NetBoxModelImportForm):
|
|
||||||
|
class SoftwareProductInstallationBulkImportForm(NetBoxModelImportForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fields = tuple()
|
fields = (
|
||||||
|
"device",
|
||||||
|
"virtualmachine",
|
||||||
|
"cluster",
|
||||||
|
"software_product",
|
||||||
|
"version",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
|
|
||||||
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
|
|
||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fieldsets = (
|
fieldsets = (FieldSet("device", "virtualmachine", "cluster", "software_product", "version"),)
|
||||||
(
|
nullable_fields = ("device", "virtualmachine", "cluster", "comments")
|
||||||
None,
|
|
||||||
(
|
tag = TagFilterField(model)
|
||||||
"software_product",
|
comments = CommentField()
|
||||||
"version",
|
|
||||||
),
|
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
||||||
),
|
virtualmachine = DynamicModelChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Virtual Machine"),
|
||||||
|
)
|
||||||
|
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
||||||
|
software_product = DynamicModelChoiceField(
|
||||||
|
queryset=SoftwareProduct.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_("Software Product"),
|
||||||
|
)
|
||||||
|
version = DynamicModelChoiceField(
|
||||||
|
queryset=SoftwareProductVersion.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||||
|
query_params=dict(software_product="$software_product"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,47 +1,129 @@
|
|||||||
from django import forms
|
from django.forms import DateField, CharField, MultipleChoiceField
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
from dcim.models import Manufacturer
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
|
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes
|
||||||
from utilities.forms.widgets import APISelect
|
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):
|
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||||
"""Form for creating a new SoftwareProductVersion object."""
|
comments = CommentField()
|
||||||
|
|
||||||
name = forms.CharField(label=_("Version"))
|
release_date = DateField(required=False, widget=DatePicker())
|
||||||
|
end_of_support = DateField(required=False, widget=DatePicker())
|
||||||
|
|
||||||
software_product = DynamicModelChoiceField(
|
software_product = DynamicModelChoiceField(
|
||||||
queryset=SoftwareProduct.objects.all(),
|
queryset=SoftwareProduct.objects.all(),
|
||||||
|
required=True,
|
||||||
|
label=_("Software Product"),
|
||||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProductVersion
|
model = SoftwareProductVersion
|
||||||
fields = ("name", "software_product", "tags")
|
fields = (
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"software_product",
|
||||||
|
"release_date",
|
||||||
|
"documentation_url",
|
||||||
|
"end_of_support",
|
||||||
|
"filename",
|
||||||
|
"file_checksum",
|
||||||
|
"file_link",
|
||||||
|
"release_type",
|
||||||
|
"tags",
|
||||||
|
"comments",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = SoftwareProductVersion
|
model = SoftwareProductVersion
|
||||||
fieldsets = ((None, ("q", "tag")),)
|
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)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
name = CharField(required=False)
|
||||||
|
description = CharField(required=False)
|
||||||
|
filename = CharField(required=False)
|
||||||
|
|
||||||
class SoftwareProductVersionImportForm(NetBoxModelImportForm):
|
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
|
||||||
class Meta:
|
|
||||||
model = SoftwareProductVersion
|
|
||||||
fields = ("name",)
|
|
||||||
|
|
||||||
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
queryset=Manufacturer.objects.all(),
|
||||||
pk = forms.ModelMultipleChoiceField(
|
required=False,
|
||||||
|
label=_("Manufacturer"),
|
||||||
|
)
|
||||||
|
software_product_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SoftwareProduct.objects.all(),
|
queryset=SoftwareProduct.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput(),
|
required=False,
|
||||||
|
label=_("Software Product"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm):
|
||||||
|
release_type = CharField(
|
||||||
|
help_text=_("Release type (possible values: %(values)s)") % {"values": SoftwareReleaseTypes.values}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SoftwareProductVersion
|
model = SoftwareProductVersion
|
||||||
nullable_fields = []
|
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,91 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.choices import JobIntervalChoices
|
||||||
|
from extras.models import Notification
|
||||||
|
from netbox.jobs import JobRunner, system_job
|
||||||
|
|
||||||
|
from netbox_slm.events import LICENSE_EXPIRING
|
||||||
|
from netbox_slm.models import SoftwareLicense, SoftwareLicenseNotificationLog
|
||||||
|
|
||||||
|
|
||||||
|
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
||||||
|
class LicenseExpirationNotificationJob(JobRunner):
|
||||||
|
class Meta:
|
||||||
|
name = "Software License Expiration Notifications"
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
today = timezone.localdate()
|
||||||
|
licenses = SoftwareLicense.objects.filter(
|
||||||
|
expiration_notifications_enabled=True,
|
||||||
|
lifetime=False,
|
||||||
|
expiration_date__gte=today,
|
||||||
|
).prefetch_related("subscriptions__user")
|
||||||
|
|
||||||
|
for license in licenses:
|
||||||
|
due_dates = [date for date in license.get_notification_dates() if date <= today]
|
||||||
|
if not due_dates:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for subscription in license.subscriptions.all():
|
||||||
|
user = subscription.user
|
||||||
|
if not user.is_active:
|
||||||
|
continue
|
||||||
|
# If notifications were enabled late, send only the most recent reminder instead of one per elapsed
|
||||||
|
# interval. Normally this still produces one reminder at each configured threshold.
|
||||||
|
self._notify(license, user, max(due_dates))
|
||||||
|
|
||||||
|
def _notify(self, license, user, notification_date):
|
||||||
|
log = SoftwareLicenseNotificationLog.objects.get_or_create(
|
||||||
|
license=license,
|
||||||
|
user=user,
|
||||||
|
expiration_date=license.expiration_date,
|
||||||
|
notification_date=notification_date,
|
||||||
|
)[0]
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
object_type = ContentType.objects.get_for_model(license)
|
||||||
|
Notification.objects.update_or_create(
|
||||||
|
object_type=object_type,
|
||||||
|
object_id=license.pk,
|
||||||
|
user=user,
|
||||||
|
defaults={
|
||||||
|
"created": now,
|
||||||
|
"object_repr": Notification.get_object_repr(license),
|
||||||
|
"event_type": LICENSE_EXPIRING,
|
||||||
|
"read": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log.netbox_sent_at = now
|
||||||
|
log.save(update_fields=("netbox_sent_at",))
|
||||||
|
self.logger.info(f"Created NetBox expiration notification for license {license.pk} and user {user}")
|
||||||
|
|
||||||
|
if license.email_notifications and user.email:
|
||||||
|
subject = _("License %(license)s expires on %(date)s") % {
|
||||||
|
"license": license,
|
||||||
|
"date": license.expiration_date,
|
||||||
|
}
|
||||||
|
body = _(
|
||||||
|
"The software license '%(license)s' for '%(product)s' expires on %(date)s."
|
||||||
|
) % {
|
||||||
|
"license": license,
|
||||||
|
"product": license.software_product,
|
||||||
|
"date": license.expiration_date,
|
||||||
|
}
|
||||||
|
if license.tenant:
|
||||||
|
body += "\n" + _("Tenant: %(tenant)s") % {"tenant": license.tenant}
|
||||||
|
if license.provider_portal_url:
|
||||||
|
body += "\n" + _("Provider portal: %(url)s") % {"url": license.provider_portal_url}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from_email = settings.SERVER_EMAIL or settings.EMAIL_HOST_USER
|
||||||
|
send_mail(subject, body, from_email, [user.email], fail_silently=False)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception(f"Failed to send expiration email for license {license.pk} to user {user}")
|
||||||
|
else:
|
||||||
|
log.email_sent_at = now
|
||||||
|
log.save(update_fields=("email_sent_at",))
|
||||||
|
self.logger.info(f"Sent expiration email for license {license.pk} to user {user}")
|
||||||
Binary file not shown.
@@ -0,0 +1,419 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: netbox-slm 1.9.1\n"
|
||||||
|
"Language: de\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
msgid "Add"
|
||||||
|
msgstr "Hinzufügen"
|
||||||
|
|
||||||
|
msgid "Import"
|
||||||
|
msgstr "Importieren"
|
||||||
|
|
||||||
|
msgid "Software Lifecycle"
|
||||||
|
msgstr "Software-Lebenszyklus"
|
||||||
|
|
||||||
|
msgid "Software Products"
|
||||||
|
msgstr "Softwareprodukte"
|
||||||
|
|
||||||
|
msgid "Software Product"
|
||||||
|
msgstr "Softwareprodukt"
|
||||||
|
|
||||||
|
msgid "Software Product Version"
|
||||||
|
msgstr "Softwareproduktversion"
|
||||||
|
|
||||||
|
msgid "Software Product Installation"
|
||||||
|
msgstr "Softwareproduktinstallation"
|
||||||
|
|
||||||
|
msgid "Software License"
|
||||||
|
msgstr "Softwarelizenz"
|
||||||
|
|
||||||
|
msgid "License Details"
|
||||||
|
msgstr "Lizenzdetails"
|
||||||
|
|
||||||
|
msgid "Versions"
|
||||||
|
msgstr "Versionen"
|
||||||
|
|
||||||
|
msgid "Version"
|
||||||
|
msgstr "Version"
|
||||||
|
|
||||||
|
msgid "Installations"
|
||||||
|
msgstr "Installationen"
|
||||||
|
|
||||||
|
msgid "Installation"
|
||||||
|
msgstr "Installation"
|
||||||
|
|
||||||
|
msgid "Add an installation"
|
||||||
|
msgstr "Installation hinzufügen"
|
||||||
|
|
||||||
|
msgid "Licenses"
|
||||||
|
msgstr "Lizenzen"
|
||||||
|
|
||||||
|
msgid "Tenancy"
|
||||||
|
msgstr "Mandantenzuordnung"
|
||||||
|
|
||||||
|
msgid "Tenant Group"
|
||||||
|
msgstr "Mandantengruppe"
|
||||||
|
|
||||||
|
msgid "Tenant"
|
||||||
|
msgstr "Mandant"
|
||||||
|
|
||||||
|
msgid "Manufacturer"
|
||||||
|
msgstr "Hersteller"
|
||||||
|
|
||||||
|
msgid "Device"
|
||||||
|
msgstr "Gerät"
|
||||||
|
|
||||||
|
msgid "Virtual Machine"
|
||||||
|
msgstr "Virtuelle Maschine"
|
||||||
|
|
||||||
|
msgid "Cluster"
|
||||||
|
msgstr "Cluster"
|
||||||
|
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "Description"
|
||||||
|
msgstr "Beschreibung"
|
||||||
|
|
||||||
|
msgid "Type"
|
||||||
|
msgstr "Typ"
|
||||||
|
|
||||||
|
msgid "SPDX expression"
|
||||||
|
msgstr "SPDX-Ausdruck"
|
||||||
|
|
||||||
|
msgid "Stored location"
|
||||||
|
msgstr "Ablageort"
|
||||||
|
|
||||||
|
msgid "Start date"
|
||||||
|
msgstr "Startdatum"
|
||||||
|
|
||||||
|
msgid "Expiration date"
|
||||||
|
msgstr "Ablaufdatum"
|
||||||
|
|
||||||
|
msgid "Support"
|
||||||
|
msgstr "Support"
|
||||||
|
|
||||||
|
msgid "License amount"
|
||||||
|
msgstr "Lizenzanzahl"
|
||||||
|
|
||||||
|
msgid "Release date"
|
||||||
|
msgstr "Veröffentlichungsdatum"
|
||||||
|
|
||||||
|
msgid "Documentation URL"
|
||||||
|
msgstr "Dokumentations-URL"
|
||||||
|
|
||||||
|
msgid "End of support"
|
||||||
|
msgstr "Supportende"
|
||||||
|
|
||||||
|
msgid "Filename"
|
||||||
|
msgstr "Dateiname"
|
||||||
|
|
||||||
|
msgid "File checksum"
|
||||||
|
msgstr "Dateiprüfsumme"
|
||||||
|
|
||||||
|
msgid "Release type"
|
||||||
|
msgstr "Veröffentlichungstyp"
|
||||||
|
|
||||||
|
msgid "Release candidate"
|
||||||
|
msgstr "Veröffentlichungskandidat"
|
||||||
|
|
||||||
|
msgid "Stable release"
|
||||||
|
msgstr "Stabile Veröffentlichung"
|
||||||
|
|
||||||
|
msgid "Alpha"
|
||||||
|
msgstr "Alpha"
|
||||||
|
|
||||||
|
msgid "Beta"
|
||||||
|
msgstr "Beta"
|
||||||
|
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "Tags"
|
||||||
|
|
||||||
|
msgid "None"
|
||||||
|
msgstr "Keine"
|
||||||
|
|
||||||
|
msgid "Link"
|
||||||
|
msgstr "Link"
|
||||||
|
|
||||||
|
msgid "Support (values other than 'True' and 'False' are ignored)"
|
||||||
|
msgstr "Support (andere Werte als ‚True‘ und ‚False‘ werden ignoriert)"
|
||||||
|
|
||||||
|
msgid "Release type (possible values: %(values)s)"
|
||||||
|
msgstr "Veröffentlichungstyp (mögliche Werte: %(values)s)"
|
||||||
|
|
||||||
|
msgid "Version '%(version)s' does not belong to %(product)s. Select a compatible version."
|
||||||
|
msgstr "Version ‚%(version)s‘ gehört nicht zu %(product)s. Wähle eine kompatible Version aus."
|
||||||
|
|
||||||
|
msgid "Installation requires exactly one platform destination."
|
||||||
|
msgstr "Eine Installation benötigt genau ein Plattformziel."
|
||||||
|
|
||||||
|
msgid "%(value)s is not a known SPDX license expression"
|
||||||
|
msgstr "%(value)s ist kein bekannter SPDX-Lizenzausdruck"
|
||||||
|
|
||||||
|
msgid "The selected tenant does not belong to the selected tenant group."
|
||||||
|
msgstr "Der ausgewählte Mandant gehört nicht zur ausgewählten Mandantengruppe."
|
||||||
|
|
||||||
|
msgid "Software Lifecycle Management"
|
||||||
|
msgstr "Software-Lebenszyklusverwaltung"
|
||||||
|
|
||||||
|
msgid "Software Lifecycle Management NetBox Plugin."
|
||||||
|
msgstr "NetBox-Plugin zur Verwaltung des Software-Lebenszyklus."
|
||||||
|
|
||||||
|
msgid "name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "comments"
|
||||||
|
msgstr "Kommentare"
|
||||||
|
|
||||||
|
msgid "description"
|
||||||
|
msgstr "Beschreibung"
|
||||||
|
|
||||||
|
msgid "manufacturer"
|
||||||
|
msgstr "Hersteller"
|
||||||
|
|
||||||
|
msgid "software product"
|
||||||
|
msgstr "Softwareprodukt"
|
||||||
|
|
||||||
|
msgid "software products"
|
||||||
|
msgstr "Softwareprodukte"
|
||||||
|
|
||||||
|
msgid "release date"
|
||||||
|
msgstr "Veröffentlichungsdatum"
|
||||||
|
|
||||||
|
msgid "documentation URL"
|
||||||
|
msgstr "Dokumentations-URL"
|
||||||
|
|
||||||
|
msgid "end of support"
|
||||||
|
msgstr "Supportende"
|
||||||
|
|
||||||
|
msgid "filename"
|
||||||
|
msgstr "Dateiname"
|
||||||
|
|
||||||
|
msgid "file checksum"
|
||||||
|
msgstr "Dateiprüfsumme"
|
||||||
|
|
||||||
|
msgid "file link"
|
||||||
|
msgstr "Dateilink"
|
||||||
|
|
||||||
|
msgid "release type"
|
||||||
|
msgstr "Veröffentlichungstyp"
|
||||||
|
|
||||||
|
msgid "software product version"
|
||||||
|
msgstr "Softwareproduktversion"
|
||||||
|
|
||||||
|
msgid "software product versions"
|
||||||
|
msgstr "Softwareproduktversionen"
|
||||||
|
|
||||||
|
msgid "device"
|
||||||
|
msgstr "Gerät"
|
||||||
|
|
||||||
|
msgid "virtual machine"
|
||||||
|
msgstr "Virtuelle Maschine"
|
||||||
|
|
||||||
|
msgid "cluster"
|
||||||
|
msgstr "Cluster"
|
||||||
|
|
||||||
|
msgid "version"
|
||||||
|
msgstr "Version"
|
||||||
|
|
||||||
|
msgid "software product installation"
|
||||||
|
msgstr "Softwareproduktinstallation"
|
||||||
|
|
||||||
|
msgid "software product installations"
|
||||||
|
msgstr "Softwareproduktinstallationen"
|
||||||
|
|
||||||
|
msgid "type"
|
||||||
|
msgstr "Typ"
|
||||||
|
|
||||||
|
msgid "stored location"
|
||||||
|
msgstr "Ablageort"
|
||||||
|
|
||||||
|
msgid "stored location URL"
|
||||||
|
msgstr "Ablageort-URL"
|
||||||
|
|
||||||
|
msgid "start date"
|
||||||
|
msgstr "Startdatum"
|
||||||
|
|
||||||
|
msgid "expiration date"
|
||||||
|
msgstr "Ablaufdatum"
|
||||||
|
|
||||||
|
msgid "support"
|
||||||
|
msgstr "Support"
|
||||||
|
|
||||||
|
msgid "license amount"
|
||||||
|
msgstr "Lizenzanzahl"
|
||||||
|
|
||||||
|
msgid "installation"
|
||||||
|
msgstr "Installation"
|
||||||
|
|
||||||
|
msgid "tenant group"
|
||||||
|
msgstr "Mandantengruppe"
|
||||||
|
|
||||||
|
msgid "tenant"
|
||||||
|
msgstr "Mandant"
|
||||||
|
|
||||||
|
msgid "software license"
|
||||||
|
msgstr "Softwarelizenz"
|
||||||
|
|
||||||
|
msgid "software licenses"
|
||||||
|
msgstr "Softwarelizenzen"
|
||||||
|
|
||||||
|
msgid "Days"
|
||||||
|
msgstr "Tage"
|
||||||
|
|
||||||
|
msgid "Months"
|
||||||
|
msgstr "Monate"
|
||||||
|
|
||||||
|
msgid "Years"
|
||||||
|
msgstr "Jahre"
|
||||||
|
|
||||||
|
msgid "lifetime license"
|
||||||
|
msgstr "Lifetime-Lizenz"
|
||||||
|
|
||||||
|
msgid "renewal interval"
|
||||||
|
msgstr "Erneuerungsintervall"
|
||||||
|
|
||||||
|
msgid "renewal interval unit"
|
||||||
|
msgstr "Einheit des Erneuerungsintervalls"
|
||||||
|
|
||||||
|
msgid "license file"
|
||||||
|
msgstr "Lizenzdatei"
|
||||||
|
|
||||||
|
msgid "license key"
|
||||||
|
msgstr "Lizenzschlüssel"
|
||||||
|
|
||||||
|
msgid "provider portal URL"
|
||||||
|
msgstr "Anbieterportal-URL"
|
||||||
|
|
||||||
|
msgid "License Credentials"
|
||||||
|
msgstr "Lizenzdaten"
|
||||||
|
|
||||||
|
msgid "Lifetime license"
|
||||||
|
msgstr "Lifetime-Lizenz"
|
||||||
|
|
||||||
|
msgid "Renewal interval"
|
||||||
|
msgstr "Erneuerungsintervall"
|
||||||
|
|
||||||
|
msgid "License file"
|
||||||
|
msgstr "Lizenzdatei"
|
||||||
|
|
||||||
|
msgid "Download license file"
|
||||||
|
msgstr "Lizenzdatei herunterladen"
|
||||||
|
|
||||||
|
msgid "License key"
|
||||||
|
msgstr "Lizenzschlüssel"
|
||||||
|
|
||||||
|
msgid "Provider Portal"
|
||||||
|
msgstr "Anbieterportal"
|
||||||
|
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Ja"
|
||||||
|
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Nein"
|
||||||
|
|
||||||
|
msgid "Renewal interval and unit must be specified together."
|
||||||
|
msgstr "Erneuerungsintervall und Einheit müssen gemeinsam angegeben werden."
|
||||||
|
|
||||||
|
msgid "A lifetime license cannot have a renewal interval or expiration date."
|
||||||
|
msgstr "Eine Lifetime-Lizenz darf kein Erneuerungsintervall und kein Ablaufdatum haben."
|
||||||
|
|
||||||
|
msgid "Platform Destination (select exactly one)"
|
||||||
|
msgstr "Plattformziel (genau eines auswählen)"
|
||||||
|
|
||||||
|
msgid "Software"
|
||||||
|
msgstr "Software"
|
||||||
|
|
||||||
|
msgid "Select either a device, a virtual machine, or a cluster."
|
||||||
|
msgstr "Wähle entweder ein Gerät, eine virtuelle Maschine oder einen Cluster aus."
|
||||||
|
|
||||||
|
msgid "Leave empty when another platform destination is selected."
|
||||||
|
msgstr "Leer lassen, wenn ein anderes Plattformziel ausgewählt wurde."
|
||||||
|
|
||||||
|
msgid "Select exactly one platform destination: device, virtual machine, or cluster."
|
||||||
|
msgstr "Wähle genau ein Plattformziel aus: Gerät, virtuelle Maschine oder Cluster."
|
||||||
|
|
||||||
|
msgid "Weeks"
|
||||||
|
msgstr "Wochen"
|
||||||
|
|
||||||
|
msgid "expiration notifications"
|
||||||
|
msgstr "Ablaufbenachrichtigungen"
|
||||||
|
|
||||||
|
msgid "default notification intervals"
|
||||||
|
msgstr "Standard-Benachrichtigungsintervalle"
|
||||||
|
|
||||||
|
msgid "custom notification interval"
|
||||||
|
msgstr "Eigene Vorwarnzeit"
|
||||||
|
|
||||||
|
msgid "custom notification interval unit"
|
||||||
|
msgstr "Einheit der eigenen Vorwarnzeit"
|
||||||
|
|
||||||
|
msgid "email notifications"
|
||||||
|
msgstr "E-Mail-Benachrichtigungen"
|
||||||
|
|
||||||
|
msgid "Expiration Notifications"
|
||||||
|
msgstr "Ablaufbenachrichtigungen"
|
||||||
|
|
||||||
|
msgid "Custom notification interval and unit must be specified together."
|
||||||
|
msgstr "Eigene Vorwarnzeit und Einheit müssen gemeinsam angegeben werden."
|
||||||
|
|
||||||
|
msgid "License expiring"
|
||||||
|
msgstr "Lizenz läuft ab"
|
||||||
|
|
||||||
|
msgid "License %(license)s expires on %(date)s"
|
||||||
|
msgstr "Lizenz %(license)s läuft am %(date)s ab"
|
||||||
|
|
||||||
|
msgid "The software license '%(license)s' for '%(product)s' expires on %(date)s."
|
||||||
|
msgstr "Die Softwarelizenz ‚%(license)s‘ für ‚%(product)s‘ läuft am %(date)s ab."
|
||||||
|
|
||||||
|
msgid "Tenant: %(tenant)s"
|
||||||
|
msgstr "Mandant: %(tenant)s"
|
||||||
|
|
||||||
|
msgid "Provider portal: %(url)s"
|
||||||
|
msgstr "Anbieterportal: %(url)s"
|
||||||
|
|
||||||
|
msgid "Expiration notifications"
|
||||||
|
msgstr "Ablaufbenachrichtigungen"
|
||||||
|
|
||||||
|
msgid "Default notification intervals"
|
||||||
|
msgstr "Standard-Benachrichtigungsintervalle"
|
||||||
|
|
||||||
|
msgid "One month and one week before expiration"
|
||||||
|
msgstr "Einen Monat und eine Woche vor Ablauf"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Deaktiviert"
|
||||||
|
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Aktiviert"
|
||||||
|
|
||||||
|
msgid "Custom notification interval"
|
||||||
|
msgstr "Eigene Vorwarnzeit"
|
||||||
|
|
||||||
|
msgid "Email notifications"
|
||||||
|
msgstr "E-Mail-Benachrichtigungen"
|
||||||
|
|
||||||
|
msgid "Notification recipients"
|
||||||
|
msgstr "Benachrichtigungsempfänger"
|
||||||
|
|
||||||
|
msgid "No subscribers. Users can subscribe to this license using the Subscribe action."
|
||||||
|
msgstr "Keine Abonnenten. Benutzer können diese Lizenz über die Aktion ‚Abonnieren‘ abonnieren."
|
||||||
|
|
||||||
|
msgid "Licences"
|
||||||
|
msgstr "Lizenzen"
|
||||||
|
|
||||||
|
msgid "Licence Management NetBox Plugin."
|
||||||
|
msgstr "NetBox-Plugin zur Lizenzverwaltung."
|
||||||
|
|
||||||
|
msgid "Can manage license email notifications"
|
||||||
|
msgstr "Kann E-Mail-Benachrichtigungen für Lizenzen verwalten"
|
||||||
|
|
||||||
|
msgid "You do not have permission to change email notifications."
|
||||||
|
msgstr "Sie haben keine Berechtigung, E-Mail-Benachrichtigungen zu ändern."
|
||||||
|
|
||||||
|
msgid "Only administrators or explicitly authorized users can change this setting."
|
||||||
|
msgstr "Nur Administratoren oder ausdrücklich berechtigte Benutzer können diese Einstellung ändern."
|
||||||
@@ -24,6 +24,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='softwareproductinstallation',
|
model_name='softwareproductinstallation',
|
||||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
|
constraint=models.CheckConstraint(condition=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,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,34 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("tenancy", "0001_initial"),
|
||||||
|
("netbox_slm", "0010_softwarelicense_spdx_expression"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="tenant_group",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="software_licenses",
|
||||||
|
to="tenancy.tenantgroup",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="tenant",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="software_licenses",
|
||||||
|
to="tenancy.tenant",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.core.validators
|
||||||
|
import netbox_slm.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("netbox_slm", "0011_softwarelicense_tenancy"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(blank=True, max_length=128),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="lifetime",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="renewal_interval",
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="renewal_interval_unit",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("days", "Days"), ("months", "Months"), ("years", "Years")],
|
||||||
|
max_length=8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="license_file",
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to="netbox_slm/licenses/"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="license_key",
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="provider_portal_url",
|
||||||
|
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import django.core.validators
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("netbox_slm", "0012_softwarelicense_renewal_and_credentials"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="expiration_notifications_enabled",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="default_notification_intervals",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="custom_notification_interval",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="custom_notification_interval_unit",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("days", "Days"), ("weeks", "Weeks"), ("months", "Months")],
|
||||||
|
max_length=8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="softwarelicense",
|
||||||
|
name="email_notifications",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SoftwareLicenseNotificationLog",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("expiration_date", models.DateField()),
|
||||||
|
("notification_date", models.DateField()),
|
||||||
|
("netbox_sent_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("email_sent_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"license",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="notification_logs",
|
||||||
|
to="netbox_slm.softwarelicense",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="softwarelicensenotificationlog",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("license", "user", "expiration_date", "notification_date"),
|
||||||
|
name="netbox_slm_license_notification_unique",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("netbox_slm", "0013_license_expiration_notifications"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="softwarelicense",
|
||||||
|
options={
|
||||||
|
"permissions": [
|
||||||
|
(
|
||||||
|
"manage_softwarelicense_email_notifications",
|
||||||
|
"Can manage license email notifications",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"verbose_name": "software license",
|
||||||
|
"verbose_name_plural": "software licenses",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
+252
-44
@@ -1,11 +1,21 @@
|
|||||||
|
import calendar
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse
|
||||||
from django.utils import safestring
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from license_expression import Licensing, get_spdx_licensing
|
||||||
|
|
||||||
from netbox.models import NetBoxModel
|
from netbox.models import NetBoxModel
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.validators import EnhancedURLValidator
|
from utilities.validators import EnhancedURLValidator
|
||||||
|
|
||||||
|
spdx_licensing: Licensing = get_spdx_licensing()
|
||||||
|
|
||||||
|
|
||||||
class LaxURLField(models.URLField):
|
class LaxURLField(models.URLField):
|
||||||
"""
|
"""
|
||||||
@@ -17,13 +27,20 @@ class LaxURLField(models.URLField):
|
|||||||
|
|
||||||
|
|
||||||
class SoftwareProduct(NetBoxModel):
|
class SoftwareProduct(NetBoxModel):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(_("name"), max_length=128)
|
||||||
description = models.CharField(max_length=255, null=True, blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
|
|
||||||
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
|
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
|
||||||
|
manufacturer = models.ForeignKey(
|
||||||
|
to="dcim.Manufacturer", verbose_name=_("manufacturer"), on_delete=models.PROTECT, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("software product")
|
||||||
|
verbose_name_plural = _("software products")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -32,28 +49,62 @@ class SoftwareProduct(NetBoxModel):
|
|||||||
|
|
||||||
def get_installation_count(self):
|
def get_installation_count(self):
|
||||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||||
return (
|
search_target = reverse(
|
||||||
safestring.mark_safe(
|
"plugins:netbox_slm:softwareproductinstallation_list", query={"software_product_id": self.pk}
|
||||||
'<a href="{url}">{count}</a>'.format(
|
|
||||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
|
||||||
count=count,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if count
|
|
||||||
else "0"
|
|
||||||
)
|
)
|
||||||
|
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwareReleaseTypes(models.TextChoices):
|
||||||
|
ALPHA = "A", _("Alpha")
|
||||||
|
BETA = "B", _("Beta")
|
||||||
|
RELEASE_CANDIDATE = "RC", _("Release candidate")
|
||||||
|
STABLE = "S", _("Stable release")
|
||||||
|
|
||||||
|
|
||||||
|
class RenewalIntervalUnits(models.TextChoices):
|
||||||
|
DAYS = "days", _("Days")
|
||||||
|
MONTHS = "months", _("Months")
|
||||||
|
YEARS = "years", _("Years")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationIntervalUnits(models.TextChoices):
|
||||||
|
DAYS = "days", _("Days")
|
||||||
|
WEEKS = "weeks", _("Weeks")
|
||||||
|
MONTHS = "months", _("Months")
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductVersion(NetBoxModel):
|
class SoftwareProductVersion(NetBoxModel):
|
||||||
name = models.CharField(max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
|
|
||||||
|
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
|
||||||
|
release_date = models.DateField(_("release date"), null=True, blank=True)
|
||||||
|
documentation_url = LaxURLField(_("documentation URL"), max_length=1024, null=True, blank=True)
|
||||||
|
end_of_support = models.DateField(_("end of support"), null=True, blank=True)
|
||||||
|
filename = models.CharField(_("filename"), max_length=64, null=True, blank=True)
|
||||||
|
file_checksum = models.CharField(_("file checksum"), max_length=128, null=True, blank=True)
|
||||||
|
file_link = LaxURLField(_("file link"), max_length=1024, null=True, blank=True)
|
||||||
|
|
||||||
|
release_type = models.CharField(
|
||||||
|
_("release type"),
|
||||||
|
max_length=3,
|
||||||
|
choices=SoftwareReleaseTypes.choices,
|
||||||
|
default=SoftwareReleaseTypes.STABLE,
|
||||||
|
)
|
||||||
|
|
||||||
software_product = models.ForeignKey(
|
software_product = models.ForeignKey(
|
||||||
to="netbox_slm.SoftwareProduct",
|
to="netbox_slm.SoftwareProduct",
|
||||||
|
verbose_name=_("software product"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("software product version")
|
||||||
|
verbose_name_plural = _("software product versions")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -62,26 +113,32 @@ class SoftwareProductVersion(NetBoxModel):
|
|||||||
|
|
||||||
def get_installation_count(self):
|
def get_installation_count(self):
|
||||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||||
return (
|
search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list", query={"version_id": self.pk})
|
||||||
safestring.mark_safe(
|
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||||
'<a href="{url}">{count}</a>'.format(
|
|
||||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
|
||||||
count=count,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if count
|
|
||||||
else "0"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SoftwareProductInstallation(NetBoxModel):
|
class SoftwareProductInstallation(NetBoxModel):
|
||||||
device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
virtualmachine = models.ForeignKey(
|
|
||||||
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
|
device = models.ForeignKey(
|
||||||
|
to="dcim.Device", verbose_name=_("device"), on_delete=models.PROTECT, null=True, blank=True
|
||||||
|
)
|
||||||
|
virtualmachine = models.ForeignKey(
|
||||||
|
to="virtualization.VirtualMachine",
|
||||||
|
verbose_name=_("virtual machine"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
cluster = models.ForeignKey(
|
||||||
|
to="virtualization.Cluster", verbose_name=_("cluster"), on_delete=models.PROTECT, null=True, blank=True
|
||||||
|
)
|
||||||
|
software_product = models.ForeignKey(
|
||||||
|
to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
version = models.ForeignKey(
|
||||||
|
to="netbox_slm.SoftwareProductVersion", verbose_name=_("version"), on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
|
|
||||||
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
|
|
||||||
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
@@ -89,15 +146,19 @@ class SoftwareProductInstallation(NetBoxModel):
|
|||||||
return f"{self.pk} ({self.platform})"
|
return f"{self.pk} ({self.platform})"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("software product installation")
|
||||||
|
verbose_name_plural = _("software product installations")
|
||||||
constraints = [
|
constraints = [
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
name="%(app_label)s_%(class)s_platform",
|
name="%(app_label)s_%(class)s_platform",
|
||||||
check=(
|
condition=(
|
||||||
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
|
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=False, cluster__isnull=True)
|
||||||
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
|
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
|
||||||
),
|
),
|
||||||
violation_error_message="Installation requires exactly one platform destination.",
|
violation_error_message=_(
|
||||||
|
"Select exactly one platform destination: device, virtual machine, or cluster."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -116,32 +177,179 @@ class SoftwareProductInstallation(NetBoxModel):
|
|||||||
return "cluster"
|
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(_("%(value)s is not a known SPDX license expression") % {"value": value})
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicense(NetBoxModel):
|
class SoftwareLicense(NetBoxModel):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(_("name"), max_length=128)
|
||||||
description = models.CharField(max_length=255, null=True, blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
type = models.CharField(max_length=128)
|
|
||||||
|
|
||||||
stored_location = models.CharField(max_length=255, null=True, blank=True)
|
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
|
||||||
stored_location_url = LaxURLField(max_length=1024, null=True, blank=True)
|
type = models.CharField(_("type"), max_length=128, blank=True)
|
||||||
start_date = models.DateField(null=True, blank=True)
|
spdx_expression = models.CharField(
|
||||||
expiration_date = models.DateField(null=True, blank=True)
|
_("SPDX expression"), max_length=64, null=True, blank=True, validators=[validate_spdx_expression]
|
||||||
|
)
|
||||||
|
stored_location = models.CharField(_("stored location"), max_length=255, null=True, blank=True)
|
||||||
|
stored_location_url = LaxURLField(_("stored location URL"), max_length=1024, null=True, blank=True)
|
||||||
|
start_date = models.DateField(_("start date"), null=True, blank=True)
|
||||||
|
expiration_date = models.DateField(_("expiration date"), null=True, blank=True)
|
||||||
|
support = models.BooleanField(_("support"), default=None, null=True, blank=True)
|
||||||
|
license_amount = models.PositiveIntegerField(_("license amount"), default=None, null=True, blank=True)
|
||||||
|
lifetime = models.BooleanField(_("lifetime license"), default=False)
|
||||||
|
renewal_interval = models.PositiveIntegerField(
|
||||||
|
_("renewal interval"), null=True, blank=True, validators=[MinValueValidator(1)]
|
||||||
|
)
|
||||||
|
renewal_interval_unit = models.CharField(
|
||||||
|
_("renewal interval unit"), max_length=8, choices=RenewalIntervalUnits.choices, blank=True
|
||||||
|
)
|
||||||
|
license_file = models.FileField(_("license file"), upload_to="netbox_slm/licenses/", null=True, blank=True)
|
||||||
|
license_key = models.TextField(_("license key"), blank=True)
|
||||||
|
provider_portal_url = LaxURLField(_("provider portal URL"), max_length=1024, null=True, blank=True)
|
||||||
|
expiration_notifications_enabled = models.BooleanField(_("expiration notifications"), default=True)
|
||||||
|
default_notification_intervals = models.BooleanField(_("default notification intervals"), default=True)
|
||||||
|
custom_notification_interval = models.PositiveIntegerField(
|
||||||
|
_("custom notification interval"), null=True, blank=True, validators=[MinValueValidator(1)]
|
||||||
|
)
|
||||||
|
custom_notification_interval_unit = models.CharField(
|
||||||
|
_("custom notification interval unit"),
|
||||||
|
max_length=8,
|
||||||
|
choices=NotificationIntervalUnits.choices,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
email_notifications = models.BooleanField(_("email notifications"), default=False)
|
||||||
|
|
||||||
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
|
software_product = models.ForeignKey(
|
||||||
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
|
to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
version = models.ForeignKey(
|
||||||
|
to="netbox_slm.SoftwareProductVersion",
|
||||||
|
verbose_name=_("version"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
installation = models.ForeignKey(
|
installation = models.ForeignKey(
|
||||||
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
|
to="netbox_slm.SoftwareProductInstallation",
|
||||||
|
verbose_name=_("installation"),
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
tenant_group = models.ForeignKey(
|
||||||
|
to="tenancy.TenantGroup",
|
||||||
|
verbose_name=_("tenant group"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="software_licenses",
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to="tenancy.Tenant",
|
||||||
|
verbose_name=_("tenant"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="software_licenses",
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("software license")
|
||||||
|
verbose_name_plural = _("software licenses")
|
||||||
|
permissions = [
|
||||||
|
("manage_softwarelicense_email_notifications", _("Can manage license email notifications")),
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.tenant_group and self.tenant and self.tenant.group_id != self.tenant_group_id:
|
||||||
|
raise ValidationError(
|
||||||
|
{"tenant": _("The selected tenant does not belong to the selected tenant group.")}
|
||||||
|
)
|
||||||
|
if bool(self.renewal_interval) != bool(self.renewal_interval_unit):
|
||||||
|
raise ValidationError(
|
||||||
|
{"renewal_interval": _("Renewal interval and unit must be specified together.")}
|
||||||
|
)
|
||||||
|
if self.lifetime and (self.renewal_interval or self.renewal_interval_unit or self.expiration_date):
|
||||||
|
raise ValidationError(
|
||||||
|
{"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")}
|
||||||
|
)
|
||||||
|
if bool(self.custom_notification_interval) != bool(self.custom_notification_interval_unit):
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"custom_notification_interval": _(
|
||||||
|
"Custom notification interval and unit must be specified together."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _subtract_months(value, months):
|
||||||
|
month_index = value.year * 12 + value.month - 1 - months
|
||||||
|
year, zero_based_month = divmod(month_index, 12)
|
||||||
|
month = zero_based_month + 1
|
||||||
|
day = min(value.day, calendar.monthrange(year, month)[1])
|
||||||
|
return value.replace(year=year, month=month, day=day)
|
||||||
|
|
||||||
|
def get_notification_dates(self):
|
||||||
|
"""Return unique reminder dates for the current expiration date."""
|
||||||
|
if not self.expiration_notifications_enabled or self.lifetime or not self.expiration_date:
|
||||||
|
return []
|
||||||
|
|
||||||
|
dates = set()
|
||||||
|
if self.default_notification_intervals:
|
||||||
|
dates.add(self._subtract_months(self.expiration_date, 1))
|
||||||
|
dates.add(self.expiration_date - timedelta(weeks=1))
|
||||||
|
|
||||||
|
if self.custom_notification_interval and self.custom_notification_interval_unit:
|
||||||
|
amount = self.custom_notification_interval
|
||||||
|
if self.custom_notification_interval_unit == NotificationIntervalUnits.MONTHS:
|
||||||
|
dates.add(self._subtract_months(self.expiration_date, amount))
|
||||||
|
elif self.custom_notification_interval_unit == NotificationIntervalUnits.WEEKS:
|
||||||
|
dates.add(self.expiration_date - timedelta(weeks=amount))
|
||||||
|
else:
|
||||||
|
dates.add(self.expiration_date - timedelta(days=amount))
|
||||||
|
|
||||||
|
return sorted(dates)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stored_location_txt(self):
|
def stored_location_txt(self):
|
||||||
if self.stored_location_url and not self.stored_location:
|
if self.stored_location_url and not self.stored_location:
|
||||||
return "Link"
|
return _("Link")
|
||||||
return self.stored_location
|
return self.stored_location
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwareLicenseNotificationLog(models.Model):
|
||||||
|
license = models.ForeignKey(
|
||||||
|
to="netbox_slm.SoftwareLicense", on_delete=models.CASCADE, related_name="notification_logs"
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
expiration_date = models.DateField()
|
||||||
|
notification_date = models.DateField()
|
||||||
|
netbox_sent_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
email_sent_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("license", "user", "expiration_date", "notification_date"),
|
||||||
|
name="netbox_slm_license_notification_unique",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|||||||
+38
-31
@@ -1,85 +1,92 @@
|
|||||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
|
||||||
from utilities.choices import ButtonColorChoices
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from . import SLMConfig
|
||||||
|
|
||||||
menu_items = (
|
slm_items = (
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link="plugins:netbox_slm:softwareproduct_list",
|
link="plugins:netbox_slm:softwareproduct_list",
|
||||||
link_text="Software Products",
|
link_text=_("Software Products"),
|
||||||
|
permissions=["netbox_slm.add_softwareproduct"],
|
||||||
buttons=(
|
buttons=(
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproduct_add",
|
"plugins:netbox_slm:softwareproduct_add",
|
||||||
"Add",
|
_("Add"),
|
||||||
"mdi mdi-plus-thick",
|
"mdi mdi-plus-thick",
|
||||||
ButtonColorChoices.GREEN,
|
|
||||||
permissions=["netbox_slm.add_softwareproduct"],
|
permissions=["netbox_slm.add_softwareproduct"],
|
||||||
),
|
),
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproduct_import",
|
"plugins:netbox_slm:softwareproduct_bulk_import",
|
||||||
"Import",
|
_("Import"),
|
||||||
"mdi mdi-upload",
|
"mdi mdi-upload",
|
||||||
ButtonColorChoices.CYAN,
|
permissions=["netbox_slm.import_softwareproduct"],
|
||||||
permissions=["netbox_slm.add_softwareproduct"],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link="plugins:netbox_slm:softwareproductversion_list",
|
link="plugins:netbox_slm:softwareproductversion_list",
|
||||||
link_text="Versions",
|
link_text=_("Versions"),
|
||||||
|
permissions=["netbox_slm.add_softwareproductversion"],
|
||||||
buttons=(
|
buttons=(
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproductversion_add",
|
"plugins:netbox_slm:softwareproductversion_add",
|
||||||
"Add",
|
_("Add"),
|
||||||
"mdi mdi-plus-thick",
|
"mdi mdi-plus-thick",
|
||||||
ButtonColorChoices.GREEN,
|
|
||||||
permissions=["netbox_slm.add_softwareproductversion"],
|
permissions=["netbox_slm.add_softwareproductversion"],
|
||||||
),
|
),
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproductversion_import",
|
"plugins:netbox_slm:softwareproductversion_bulk_import",
|
||||||
"Import",
|
_("Import"),
|
||||||
"mdi mdi-upload",
|
"mdi mdi-upload",
|
||||||
ButtonColorChoices.CYAN,
|
permissions=["netbox_slm.import_softwareproductversion"],
|
||||||
permissions=["netbox_slm.add_softwareproductversion"],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link="plugins:netbox_slm:softwareproductinstallation_list",
|
link="plugins:netbox_slm:softwareproductinstallation_list",
|
||||||
link_text="Installations",
|
link_text=_("Installations"),
|
||||||
|
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||||
buttons=(
|
buttons=(
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproductinstallation_add",
|
"plugins:netbox_slm:softwareproductinstallation_add",
|
||||||
"Add",
|
_("Add"),
|
||||||
"mdi mdi-plus-thick",
|
"mdi mdi-plus-thick",
|
||||||
ButtonColorChoices.GREEN,
|
|
||||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||||
),
|
),
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwareproductinstallation_import",
|
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
|
||||||
"Import",
|
_("Import"),
|
||||||
"mdi mdi-upload",
|
"mdi mdi-upload",
|
||||||
ButtonColorChoices.CYAN,
|
permissions=["netbox_slm.import_softwareproductinstallation"],
|
||||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link="plugins:netbox_slm:softwarelicense_list",
|
link="plugins:netbox_slm:softwarelicense_list",
|
||||||
link_text="Licenses",
|
link_text=_("Licenses"),
|
||||||
|
permissions=["netbox_slm.add_softwarelicense"],
|
||||||
buttons=(
|
buttons=(
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwarelicense_add",
|
"plugins:netbox_slm:softwarelicense_add",
|
||||||
"Add",
|
_("Add"),
|
||||||
"mdi mdi-plus-thick",
|
"mdi mdi-plus-thick",
|
||||||
ButtonColorChoices.GREEN,
|
|
||||||
permissions=["netbox_slm.add_softwarelicense"],
|
permissions=["netbox_slm.add_softwarelicense"],
|
||||||
),
|
),
|
||||||
PluginMenuButton(
|
PluginMenuButton(
|
||||||
"plugins:netbox_slm:softwarelicense_import",
|
"plugins:netbox_slm:softwarelicense_bulk_import",
|
||||||
"Import",
|
_("Import"),
|
||||||
"mdi mdi-upload",
|
"mdi mdi-upload",
|
||||||
ButtonColorChoices.CYAN,
|
permissions=["netbox_slm.import_softwarelicense"],
|
||||||
permissions=["netbox_slm.add_softwarelicense"],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if get_plugin_config("netbox_slm", "top_level_menu"):
|
||||||
|
menu = PluginMenu(
|
||||||
|
label=_("Licences"),
|
||||||
|
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
|
||||||
|
|||||||
+41
-34
@@ -1,5 +1,6 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.db.models import Count, F, Value
|
from django.db.models import Count, F, Value
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, ToggleColumn, columns
|
from netbox.tables import NetBoxTable, ToggleColumn, columns
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||||
@@ -11,7 +12,7 @@ class SoftwareProductTable(NetBoxTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
|
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
|
||||||
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
|
installations = tables.Column(accessor="get_installation_count")
|
||||||
|
|
||||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
|
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
|
||||||
|
|
||||||
@@ -20,8 +21,8 @@ class SoftwareProductTable(NetBoxTable):
|
|||||||
fields = (
|
fields = (
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"manufacturer",
|
|
||||||
"description",
|
"description",
|
||||||
|
"manufacturer",
|
||||||
"installations",
|
"installations",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
@@ -29,16 +30,9 @@ class SoftwareProductTable(NetBoxTable):
|
|||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
"description",
|
|
||||||
"installations",
|
"installations",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
sequence = (
|
|
||||||
"manufacturer",
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"installations",
|
|
||||||
)
|
|
||||||
|
|
||||||
def order_installations(self, queryset, is_descending):
|
def order_installations(self, queryset, is_descending):
|
||||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||||
@@ -51,10 +45,10 @@ class SoftwareProductVersionTable(NetBoxTable):
|
|||||||
"""Table for displaying SoftwareProductVersion objects."""
|
"""Table for displaying SoftwareProductVersion objects."""
|
||||||
|
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name="Version")
|
name = tables.LinkColumn()
|
||||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||||
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
|
installations = tables.Column(accessor="get_installation_count")
|
||||||
|
|
||||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
|
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
|
||||||
|
|
||||||
@@ -63,25 +57,24 @@ class SoftwareProductVersionTable(NetBoxTable):
|
|||||||
fields = (
|
fields = (
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"software_product",
|
"software_product",
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
|
"release_date",
|
||||||
|
"end_of_support",
|
||||||
|
"release_type",
|
||||||
"installations",
|
"installations",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"software_product",
|
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
|
"software_product",
|
||||||
|
"release_date",
|
||||||
"installations",
|
"installations",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
sequence = (
|
|
||||||
"manufacturer",
|
|
||||||
"software_product",
|
|
||||||
"name",
|
|
||||||
"installations",
|
|
||||||
)
|
|
||||||
|
|
||||||
def order_installations(self, queryset, is_descending):
|
def order_installations(self, queryset, is_descending):
|
||||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||||
@@ -94,14 +87,10 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
|||||||
"""Table for displaying SoftwareProductInstallation objects."""
|
"""Table for displaying SoftwareProductInstallation objects."""
|
||||||
|
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
|
||||||
|
|
||||||
device = tables.Column(accessor="device", linkify=True)
|
|
||||||
virtualmachine = tables.Column(accessor="virtualmachine", linkify=True)
|
|
||||||
cluster = tables.Column(accessor="cluster", linkify=True)
|
|
||||||
platform = tables.Column(accessor="platform", linkify=True)
|
platform = tables.Column(accessor="platform", linkify=True)
|
||||||
type = tables.Column(accessor="render_type")
|
type = tables.Column(accessor="render_type")
|
||||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||||
|
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||||
version = tables.Column(accessor="version", linkify=True)
|
version = tables.Column(accessor="version", linkify=True)
|
||||||
|
|
||||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
|
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
|
||||||
@@ -110,9 +99,9 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
|||||||
model = SoftwareProductInstallation
|
model = SoftwareProductInstallation
|
||||||
fields = (
|
fields = (
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
|
||||||
"platform",
|
"platform",
|
||||||
"type",
|
"type",
|
||||||
|
"manufacturer",
|
||||||
"software_product",
|
"software_product",
|
||||||
"version",
|
"version",
|
||||||
"tags",
|
"tags",
|
||||||
@@ -121,6 +110,7 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
|||||||
"pk",
|
"pk",
|
||||||
"platform",
|
"platform",
|
||||||
"type",
|
"type",
|
||||||
|
"manufacturer",
|
||||||
"software_product",
|
"software_product",
|
||||||
"version",
|
"version",
|
||||||
"tags",
|
"tags",
|
||||||
@@ -140,9 +130,6 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
|||||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||||
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
|
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
|
||||||
|
|
||||||
def render_software_product(self, value, **kwargs):
|
|
||||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseTable(NetBoxTable):
|
class SoftwareLicenseTable(NetBoxTable):
|
||||||
"""Table for displaying SoftwareLicense objects."""
|
"""Table for displaying SoftwareLicense objects."""
|
||||||
@@ -151,11 +138,16 @@ class SoftwareLicenseTable(NetBoxTable):
|
|||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
|
|
||||||
type = tables.Column()
|
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)
|
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
|
||||||
|
|
||||||
software_product = tables.Column(accessor="software_product", linkify=True)
|
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||||
|
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||||
version = tables.Column(accessor="version", linkify=True)
|
version = tables.Column(accessor="version", linkify=True)
|
||||||
installation = tables.Column(accessor="installation", linkify=True)
|
installation = tables.Column(accessor="installation", linkify=True)
|
||||||
|
tenant_group = tables.Column(accessor="tenant_group", verbose_name=_("Tenant Group"), linkify=True)
|
||||||
|
tenant = tables.Column(accessor="tenant", linkify=True)
|
||||||
|
provider_portal_url = tables.Column(verbose_name=_("Provider Portal"), linkify=True)
|
||||||
|
|
||||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
|
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
|
||||||
|
|
||||||
@@ -166,25 +158,40 @@ class SoftwareLicenseTable(NetBoxTable):
|
|||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"type",
|
"type",
|
||||||
|
"spdx_expression",
|
||||||
"stored_location",
|
"stored_location",
|
||||||
"start_date",
|
"start_date",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"manufacturer",
|
||||||
"software_product",
|
"software_product",
|
||||||
"version",
|
"version",
|
||||||
"installation",
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
|
"support",
|
||||||
|
"license_amount",
|
||||||
|
"provider_portal_url",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
|
"email_notifications",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"expiration_date",
|
"type",
|
||||||
|
"manufacturer",
|
||||||
"software_product",
|
"software_product",
|
||||||
"installation",
|
"installation",
|
||||||
|
"tenant_group",
|
||||||
|
"tenant",
|
||||||
|
"expiration_date",
|
||||||
|
"lifetime",
|
||||||
|
"expiration_notifications_enabled",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_software_product(self, value, **kwargs):
|
|
||||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
|
||||||
|
|
||||||
def render_installation(self, **kwargs):
|
def render_installation(self, **kwargs):
|
||||||
return f"{kwargs['record'].installation.platform}"
|
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,22 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "Installations" %}
|
||||||
|
{% if perms.netbox_slm.add_softwareproductinstallation %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "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>
|
||||||
@@ -3,67 +3,163 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Software License
|
{% trans "Software License" %}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<table class="table table-hover attr-table">
|
||||||
<table class="table table-hover attr-table">
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
<th scope="row">Name</th>
|
<td>{{ object.name }}</td>
|
||||||
<td>{{ object.name }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<th scope="row">Description</th>
|
<td>{{ object.description }}</td>
|
||||||
<td>{{ object.description }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Type" %}</th>
|
||||||
<th scope="row">Type</th>
|
<td>{{ object.type }}</td>
|
||||||
<td>{{ object.type }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "SPDX expression" %}</th>
|
||||||
<th scope="row">Stored location</th>
|
<td>{{ object.spdx_expression }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Software Product" %}</th>
|
||||||
|
<td>{{ object.software_product|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Version" %}</th>
|
||||||
|
<td>{{ object.version|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Installation" %}</th>
|
||||||
|
<td>{{ object.installation|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Tenant Group" %}</th>
|
||||||
|
<td>{{ object.tenant_group|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Tenant" %}</th>
|
||||||
|
<td>{{ object.tenant|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Stored location" %}</th>
|
||||||
{% if object.stored_location_url %}
|
{% if object.stored_location_url %}
|
||||||
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
|
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{ object.stored_location_txt }}</td>
|
<td>{{ object.stored_location_txt }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Start date</th>
|
<th scope="row">{% trans "Start date" %}</th>
|
||||||
<td>{{ object.start_date }}</td>
|
<td>{{ object.start_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Expiration date</th>
|
<th scope="row">{% trans "Expiration date" %}</th>
|
||||||
<td>{{ object.expiration_date }}</td>
|
<td>{{ object.expiration_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Software Product</th>
|
<th scope="row">{% trans "Lifetime license" %}</th>
|
||||||
<td>{{ object.software_product }}</td>
|
<td>{% if object.lifetime %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Version</th>
|
<th scope="row">{% trans "Renewal interval" %}</th>
|
||||||
<td>{{ object.version }}</td>
|
<td>
|
||||||
</tr>
|
{% if object.renewal_interval %}
|
||||||
<tr>
|
{{ object.renewal_interval }} {{ object.get_renewal_interval_unit_display }}
|
||||||
<th scope="row">Installation</th>
|
{% else %}
|
||||||
<td>{{ object.installation }}</td>
|
{{ None|placeholder }}
|
||||||
</tr>
|
{% endif %}
|
||||||
</table>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Support" %}</th>
|
||||||
|
<td>{{ object.support }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "License amount" %}</th>
|
||||||
|
<td>{{ object.license_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "License file" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.license_file %}
|
||||||
|
<a href="{{ object.license_file.url }}">{% trans "Download license file" %}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ None|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "License key" %}</th>
|
||||||
|
<td>{% if object.license_key %}<code>{{ object.license_key }}</code>{% else %}{{ None|placeholder }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider Portal" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.provider_portal_url %}
|
||||||
|
<a href="{{ object.provider_portal_url }}">{{ object.provider_portal_url }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ None|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Expiration notifications" %}</th>
|
||||||
|
<td>{% if object.expiration_notifications_enabled %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Default notification intervals" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.default_notification_intervals %}
|
||||||
|
{% trans "One month and one week before expiration" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Disabled" %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Custom notification interval" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.custom_notification_interval %}
|
||||||
|
{{ object.custom_notification_interval }} {{ object.get_custom_notification_interval_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ None|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Email notifications" %}</th>
|
||||||
|
<td>{% if object.email_notifications %}{% trans "Enabled" %}{% else %}{% trans "Disabled" %}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Notification recipients" %}</th>
|
||||||
|
<td>
|
||||||
|
{% for subscription in object.subscriptions.all %}
|
||||||
|
{{ subscription.user|linkify }}{% if not forloop.last %}, {% endif %}
|
||||||
|
{% empty %}
|
||||||
|
{% trans "No subscribers. Users can subscribe to this license using the Subscribe action." %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,51 +3,75 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Software Product
|
{% trans "Software Product" %}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<table class="table table-hover attr-table">
|
||||||
<table class="table table-hover attr-table">
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
<th scope="row">Name</th>
|
<td>{{ object.name }}</td>
|
||||||
<td>{{ object.name }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<th scope="row">Description</th>
|
<td>{{ object.description }}</td>
|
||||||
<td>{{ object.description }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||||
<th scope="row">Manufacturer</th>
|
<td>{{ object.manufacturer|linkify }}</td>
|
||||||
<td>{{ object.manufacturer }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Versions" %}</th>
|
||||||
<th scope="row">Versions</th>
|
<td>
|
||||||
<td>
|
{% for version in object.softwareproductversion_set.all %}
|
||||||
{% for version in object.softwareproduct_versions.all %}
|
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
|
||||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
{{ version }}
|
||||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
</a>
|
||||||
</a>
|
{% empty %}
|
||||||
{% empty %}
|
{% trans "None" %}
|
||||||
n/a
|
{% endfor %}
|
||||||
{% endfor %}
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
</table>
|
<th scope="row">{% trans "Installations" %}</th>
|
||||||
</div>
|
<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 %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "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 %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,51 +3,63 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Software Product Installation
|
{% trans "Software Product Installation" %}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<table class="table table-hover attr-table">
|
||||||
<table class="table table-hover attr-table">
|
|
||||||
{% if object.device %}
|
{% if object.device %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Device</th>
|
<th scope="row">{% trans "Device" %}</th>
|
||||||
<td>{{ object.device }}</td>
|
<td>{{ object.device|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif object.virtualmachine %}
|
{% elif object.virtualmachine %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Virtualmachine</th>
|
<th scope="row">{% trans "Virtual Machine" %}</th>
|
||||||
<td>{{ object.virtualmachine }}</td>
|
<td>{{ object.virtualmachine|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Cluster</th>
|
<th scope="row">{% trans "Cluster" %}</th>
|
||||||
<td>{{ object.cluster }}</td>
|
<td>{{ object.cluster|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Software Product</th>
|
<th scope="row">{% trans "Software Product" %}</th>
|
||||||
<td>{{ object.software_product }}</td>
|
<td>{{ object.software_product|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Version</th>
|
<th scope="row">{% trans "Version" %}</th>
|
||||||
<td>{{ object.version }}</td>
|
<td>{{ object.version|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
<tr>
|
||||||
</div>
|
<th scope="row">{% trans "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 %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,37 +3,99 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Software Product Version
|
{% trans "Software Product Version" %}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<table class="table table-hover attr-table">
|
||||||
<table class="table table-hover attr-table">
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
<th scope="row">Name</th>
|
<td>{{ object.name }}</td>
|
||||||
<td>{{ object.name }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<th scope="row">Installations</th>
|
<td>{{ object.description }}</td>
|
||||||
<td>
|
</tr>
|
||||||
{{ object.get_installation_count }}
|
<tr>
|
||||||
</td>
|
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||||
</tr>
|
<td>{{ object.software_product.manufacturer|linkify }}</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
|
<th scope="row">{% trans "Software Product" %}</th>
|
||||||
|
<td>{{ object.software_product|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Release date" %}</th>
|
||||||
|
<td>{{ object.release_date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "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">{% trans "End of support" %}</th>
|
||||||
|
<td>{{ object.end_of_support }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "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">{% trans "File checksum" %}</th>
|
||||||
|
<td>{{ object.file_checksum }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Release type" %}</th>
|
||||||
|
<td>{{ object.get_release_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "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 %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "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 %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
|
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
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.tenant_group = TenantGroup.objects.create(name="test tenant group", slug="test-tenant-group")
|
||||||
|
cls.tenant = Tenant.objects.create(name="test tenant", slug="test-tenant", group=cls.tenant_group)
|
||||||
|
|
||||||
|
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,
|
||||||
|
tenant_group=cls.tenant_group,
|
||||||
|
tenant=cls.tenant,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
SoftwareLicense.objects.all().delete()
|
||||||
|
SoftwareProductInstallation.objects.all().delete()
|
||||||
|
SoftwareProductVersion.objects.all().delete()
|
||||||
|
SoftwareProduct.objects.all().delete()
|
||||||
|
Tenant.objects.all().delete()
|
||||||
|
TenantGroup.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")
|
||||||
+174
-41
@@ -1,40 +1,29 @@
|
|||||||
from django.test import TestCase
|
from datetime import date, timedelta
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from django.contrib.auth import get_user_model
|
||||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext, override
|
||||||
|
from extras.models import Notification, Subscription
|
||||||
|
|
||||||
|
from netbox_slm.forms import SoftwareLicenseForm, SoftwareProductInstallationForm
|
||||||
|
from netbox_slm.jobs import LicenseExpirationNotificationJob
|
||||||
|
from netbox_slm.models import (
|
||||||
|
NotificationIntervalUnits,
|
||||||
|
RenewalIntervalUnits,
|
||||||
|
SoftwareLicense,
|
||||||
|
SoftwareLicenseNotificationLog,
|
||||||
|
)
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
from .base import SlmBaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class ModelTestCase(TestCase):
|
class ModelTestCase(SlmBaseTestCase):
|
||||||
def setUp(self):
|
"""Test basic model functionality and custom overrides"""
|
||||||
self.p_name = "test product"
|
|
||||||
self.v_name = "test version"
|
|
||||||
self.l_name = "test license"
|
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name="test manufacturer")
|
|
||||||
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
|
|
||||||
device_role = DeviceRole.objects.create(name="test device role")
|
|
||||||
site = Site.objects.create(name="test site")
|
|
||||||
self.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
|
|
||||||
self.vm = VirtualMachine.objects.create(name="test VM")
|
|
||||||
cluster_type = ClusterType.objects.create(name="test cluster type")
|
|
||||||
self.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
|
|
||||||
self.test_url = "https://github.com/ICTU/netbox_slm"
|
|
||||||
|
|
||||||
self.software_product = SoftwareProduct.objects.create(name=self.p_name)
|
|
||||||
self.software_product_version = SoftwareProductVersion.objects.create(
|
|
||||||
name=self.v_name, software_product=self.software_product
|
|
||||||
)
|
|
||||||
self.software_product_installation = SoftwareProductInstallation.objects.create(
|
|
||||||
virtualmachine=self.vm, software_product=self.software_product, version=self.software_product_version
|
|
||||||
)
|
|
||||||
self.software_license = SoftwareLicense.objects.create(
|
|
||||||
name=self.l_name,
|
|
||||||
software_product=self.software_product,
|
|
||||||
version=self.software_product_version,
|
|
||||||
installation=self.software_product_installation,
|
|
||||||
stored_location_url=self.test_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_model_name(self):
|
def test_model_name(self):
|
||||||
self.assertEqual(self.p_name, str(self.software_product))
|
self.assertEqual(self.p_name, str(self.software_product))
|
||||||
@@ -43,15 +32,28 @@ class ModelTestCase(TestCase):
|
|||||||
self.assertEqual(self.l_name, str(self.software_license))
|
self.assertEqual(self.l_name, str(self.software_license))
|
||||||
|
|
||||||
def test_absolute_url(self):
|
def test_absolute_url(self):
|
||||||
self.assertEqual("/plugins/slm/software-products/1/", self.software_product.get_absolute_url())
|
self.assertEqual(
|
||||||
self.assertEqual("/plugins/slm/versions/1/", self.software_product_version.get_absolute_url())
|
f"/plugins/slm/software-products/{self.software_product.pk}/", self.software_product.get_absolute_url()
|
||||||
self.assertEqual("/plugins/slm/installations/1/", self.software_product_installation.get_absolute_url())
|
)
|
||||||
self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url())
|
self.assertEqual(
|
||||||
|
f"/plugins/slm/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):
|
def test_get_installation_count(self):
|
||||||
installation_ss = '<a href="/plugins/slm/installations/?q={}">1</a>'
|
self.assertEqual(
|
||||||
self.assertEqual(installation_ss.format(self.p_name), self.software_product.get_installation_count())
|
f"<a href='/plugins/slm/installations/?software_product_id={self.software_product.pk}'>1</a>",
|
||||||
self.assertEqual(installation_ss.format(self.v_name), self.software_product_version.get_installation_count())
|
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):
|
def test_product_installation_methods(self):
|
||||||
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
||||||
@@ -69,9 +71,140 @@ class ModelTestCase(TestCase):
|
|||||||
self.assertEqual("cluster", self.software_product_installation.render_type())
|
self.assertEqual("cluster", self.software_product_installation.render_type())
|
||||||
self.assertEqual(self.cluster, self.software_product_installation.platform)
|
self.assertEqual(self.cluster, self.software_product_installation.platform)
|
||||||
|
|
||||||
|
def test_installation_form_requires_exactly_one_platform_destination(self):
|
||||||
|
base_data = {
|
||||||
|
"software_product": self.software_product.pk,
|
||||||
|
"version": self.software_product_version.pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
no_destination_form = SoftwareProductInstallationForm(data=base_data)
|
||||||
|
self.assertFalse(no_destination_form.is_valid())
|
||||||
|
self.assertIn("Select exactly one platform destination", str(no_destination_form.non_field_errors()))
|
||||||
|
|
||||||
|
one_destination_form = SoftwareProductInstallationForm(data={**base_data, "device": self.device.pk})
|
||||||
|
self.assertTrue(one_destination_form.is_valid(), one_destination_form.errors)
|
||||||
|
|
||||||
|
multiple_destinations_form = SoftwareProductInstallationForm(
|
||||||
|
data={**base_data, "device": self.device.pk, "cluster": self.cluster.pk}
|
||||||
|
)
|
||||||
|
self.assertFalse(multiple_destinations_form.is_valid())
|
||||||
|
self.assertIn("Select exactly one platform destination", str(multiple_destinations_form.non_field_errors()))
|
||||||
|
|
||||||
def test_software_license_stored_location_txt(self):
|
def test_software_license_stored_location_txt(self):
|
||||||
self.assertEqual("Link", self.software_license.stored_location_txt)
|
self.assertEqual("Link", self.software_license.stored_location_txt)
|
||||||
|
|
||||||
self.software_license.stored_location = "GitHub"
|
self.software_license.stored_location = "GitHub"
|
||||||
self.software_license.save()
|
self.software_license.save()
|
||||||
self.assertEqual("GitHub", self.software_license.stored_location_txt)
|
self.assertEqual("GitHub", self.software_license.stored_location_txt)
|
||||||
|
|
||||||
|
def test_software_license_tenancy(self):
|
||||||
|
self.assertEqual(self.tenant_group, self.software_license.tenant_group)
|
||||||
|
self.assertEqual(self.tenant, self.software_license.tenant)
|
||||||
|
|
||||||
|
def test_software_license_form_renders_tenancy_fields(self):
|
||||||
|
form = SoftwareLicenseForm(instance=self.software_license)
|
||||||
|
self.assertIn("tenant_group", form.fields)
|
||||||
|
self.assertIn("tenant", form.fields)
|
||||||
|
self.assertTrue(
|
||||||
|
any("tenant_group" in fieldset.items and "tenant" in fieldset.items for fieldset in form.fieldsets)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_software_license_form_contains_renewal_and_credentials(self):
|
||||||
|
form = SoftwareLicenseForm(instance=self.software_license)
|
||||||
|
for field_name in (
|
||||||
|
"lifetime",
|
||||||
|
"renewal_interval",
|
||||||
|
"renewal_interval_unit",
|
||||||
|
"license_file",
|
||||||
|
"license_key",
|
||||||
|
"provider_portal_url",
|
||||||
|
):
|
||||||
|
self.assertIn(field_name, form.fields)
|
||||||
|
|
||||||
|
def test_software_license_accepts_optional_type_and_renewal_interval(self):
|
||||||
|
license = SoftwareLicense(
|
||||||
|
name="renewable license",
|
||||||
|
software_product=self.software_product,
|
||||||
|
renewal_interval=12,
|
||||||
|
renewal_interval_unit=RenewalIntervalUnits.MONTHS,
|
||||||
|
)
|
||||||
|
license.full_clean()
|
||||||
|
|
||||||
|
def test_software_license_requires_renewal_interval_and_unit_together(self):
|
||||||
|
license = SoftwareLicense(
|
||||||
|
name="invalid renewable license",
|
||||||
|
software_product=self.software_product,
|
||||||
|
renewal_interval=12,
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, "must be specified together"):
|
||||||
|
license.full_clean()
|
||||||
|
|
||||||
|
def test_lifetime_license_rejects_expiration(self):
|
||||||
|
license = SoftwareLicense(
|
||||||
|
name="invalid lifetime license",
|
||||||
|
software_product=self.software_product,
|
||||||
|
lifetime=True,
|
||||||
|
expiration_date="2030-01-01",
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, "cannot have a renewal interval or expiration date"):
|
||||||
|
license.full_clean()
|
||||||
|
|
||||||
|
def test_license_notification_dates(self):
|
||||||
|
license = SoftwareLicense(
|
||||||
|
name="expiring license",
|
||||||
|
software_product=self.software_product,
|
||||||
|
expiration_date=date(2027, 3, 31),
|
||||||
|
custom_notification_interval=6,
|
||||||
|
custom_notification_interval_unit=NotificationIntervalUnits.MONTHS,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[date(2026, 9, 30), date(2027, 2, 28), date(2027, 3, 24)],
|
||||||
|
license.get_notification_dates(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(SERVER_EMAIL="netbox@example.com", EMAIL_HOST_USER="smtp-user")
|
||||||
|
@patch("netbox_slm.jobs.send_mail")
|
||||||
|
def test_expiration_job_notifies_subscribers_on_every_run(self, send_mail):
|
||||||
|
user = get_user_model().objects.create_user(
|
||||||
|
username="license-recipient",
|
||||||
|
email="recipient@example.com",
|
||||||
|
)
|
||||||
|
license = SoftwareLicense.objects.create(
|
||||||
|
name="expiring subscribed license",
|
||||||
|
software_product=self.software_product,
|
||||||
|
expiration_date=timezone.localdate() + timedelta(days=1),
|
||||||
|
default_notification_intervals=False,
|
||||||
|
custom_notification_interval=1,
|
||||||
|
custom_notification_interval_unit=NotificationIntervalUnits.DAYS,
|
||||||
|
email_notifications=True,
|
||||||
|
)
|
||||||
|
Subscription.objects.create(
|
||||||
|
user=user,
|
||||||
|
object_type=ContentType.objects.get_for_model(license),
|
||||||
|
object_id=license.pk,
|
||||||
|
)
|
||||||
|
runner = LicenseExpirationNotificationJob.__new__(LicenseExpirationNotificationJob)
|
||||||
|
runner.logger = Mock()
|
||||||
|
|
||||||
|
runner.run()
|
||||||
|
runner.run()
|
||||||
|
|
||||||
|
notification = Notification.objects.get(user=user, object_id=license.pk)
|
||||||
|
self.assertIsInstance(str(notification.event), str)
|
||||||
|
self.assertEqual(1, SoftwareLicenseNotificationLog.objects.filter(user=user, license=license).count())
|
||||||
|
self.assertEqual(2, send_mail.call_count)
|
||||||
|
self.assertEqual("netbox@example.com", send_mail.call_args.args[2])
|
||||||
|
|
||||||
|
def test_german_translation(self):
|
||||||
|
with override("de"):
|
||||||
|
self.assertEqual("Softwarelizenz", gettext("Software License"))
|
||||||
|
self.assertEqual("Mandantengruppe", gettext("Tenant Group"))
|
||||||
|
self.assertEqual("Lizenzen", gettext("Licenses"))
|
||||||
|
|
||||||
|
def test_software_license_rejects_tenant_from_other_group(self):
|
||||||
|
other_group = TenantGroup.objects.create(name="other group", slug="other-group")
|
||||||
|
other_tenant = Tenant.objects.create(name="other tenant", slug="other-tenant", group=other_group)
|
||||||
|
self.software_license.tenant = other_tenant
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(ValidationError, "does not belong to the selected tenant group"):
|
||||||
|
self.software_license.full_clean()
|
||||||
|
|||||||
+11
-110
@@ -1,114 +1,15 @@
|
|||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
|
||||||
from netbox.views.generic import ObjectChangeLogView
|
from netbox_slm import views # noqa F401
|
||||||
from netbox_slm import views
|
from utilities.urls import get_model_urls
|
||||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# SoftwareProduct
|
path("software-products/", include(get_model_urls("netbox_slm", "softwareproduct", detail=False))),
|
||||||
path("software-products/", views.SoftwareProductListView.as_view(), name="softwareproduct_list"),
|
path("software-products/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproduct"))),
|
||||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name="softwareproduct_add"),
|
path("versions/", include(get_model_urls("netbox_slm", "softwareproductversion", detail=False))),
|
||||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
path("versions/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductversion"))),
|
||||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
path("installations/", include(get_model_urls("netbox_slm", "softwareproductinstallation", detail=False))),
|
||||||
path(
|
path("installations/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductinstallation"))),
|
||||||
"software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"
|
path("licenses/", include(get_model_urls("netbox_slm", "softwarelicense", detail=False))),
|
||||||
),
|
path("licenses/<int:pk>/", include(get_model_urls("netbox_slm", "softwarelicense"))),
|
||||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
|
||||||
path(
|
|
||||||
"software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"
|
|
||||||
),
|
|
||||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
|
||||||
path(
|
|
||||||
"software-products/<int:pk>/changelog/",
|
|
||||||
ObjectChangeLogView.as_view(),
|
|
||||||
name="softwareproduct_changelog",
|
|
||||||
kwargs={"model": SoftwareProduct},
|
|
||||||
),
|
|
||||||
# SoftwareProductVersion
|
|
||||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name="softwareproductversion_list"),
|
|
||||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_add"),
|
|
||||||
path(
|
|
||||||
"versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"
|
|
||||||
),
|
|
||||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
|
||||||
path(
|
|
||||||
"versions/delete/",
|
|
||||||
views.SoftwareProductVersionBulkDeleteView.as_view(),
|
|
||||||
name="softwareproductversion_bulk_delete",
|
|
||||||
),
|
|
||||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
|
||||||
path(
|
|
||||||
"versions/<int:pk>/delete/",
|
|
||||||
views.SoftwareProductVersionDeleteView.as_view(),
|
|
||||||
name="softwareproductversion_delete",
|
|
||||||
),
|
|
||||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
|
||||||
path(
|
|
||||||
"versions/<int:pk>/changelog/",
|
|
||||||
ObjectChangeLogView.as_view(),
|
|
||||||
name="softwareproductversion_changelog",
|
|
||||||
kwargs={"model": SoftwareProductVersion},
|
|
||||||
),
|
|
||||||
# SoftwareProductInstallation
|
|
||||||
path(
|
|
||||||
"installations/",
|
|
||||||
views.SoftwareProductInstallationListView.as_view(),
|
|
||||||
name="softwareproductinstallation_list",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/add/",
|
|
||||||
views.SoftwareProductInstallationEditView.as_view(),
|
|
||||||
name="softwareproductinstallation_add",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/import/",
|
|
||||||
views.SoftwareProductInstallationBulkImportView.as_view(),
|
|
||||||
name="softwareproductinstallation_import",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/edit/",
|
|
||||||
views.SoftwareProductInstallationBulkEditView.as_view(),
|
|
||||||
name="softwareproductinstallation_bulk_edit",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/delete/",
|
|
||||||
views.SoftwareProductInstallationBulkDeleteView.as_view(),
|
|
||||||
name="softwareproductinstallation_bulk_delete",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/<int:pk>/",
|
|
||||||
views.SoftwareProductInstallationView.as_view(),
|
|
||||||
name="softwareproductinstallation",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/<int:pk>/delete/",
|
|
||||||
views.SoftwareProductInstallationDeleteView.as_view(),
|
|
||||||
name="softwareproductinstallation_delete",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/<int:pk>/edit/",
|
|
||||||
views.SoftwareProductInstallationEditView.as_view(),
|
|
||||||
name="softwareproductinstallation_edit",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"installations/<int:pk>/changelog/",
|
|
||||||
ObjectChangeLogView.as_view(),
|
|
||||||
name="softwareproductinstallation_changelog",
|
|
||||||
kwargs={"model": SoftwareProductInstallation},
|
|
||||||
),
|
|
||||||
# SoftwareLicense
|
|
||||||
path("licenses/", views.SoftwareLicenseListView.as_view(), name="softwarelicense_list"),
|
|
||||||
path("licenses/add/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_add"),
|
|
||||||
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(), name="softwarelicense_import"),
|
|
||||||
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(), name="softwarelicense_bulk_edit"),
|
|
||||||
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(), name="softwarelicense_bulk_delete"),
|
|
||||||
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(), name="softwarelicense"),
|
|
||||||
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(), name="softwarelicense_delete"),
|
|
||||||
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_edit"),
|
|
||||||
path(
|
|
||||||
"licenses/<int:pk>/changelog/",
|
|
||||||
ObjectChangeLogView.as_view(),
|
|
||||||
name="softwarelicense_changelog",
|
|
||||||
kwargs={"model": SoftwareLicense},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,34 +3,30 @@ from .software_license import (
|
|||||||
SoftwareLicenseView,
|
SoftwareLicenseView,
|
||||||
SoftwareLicenseEditView,
|
SoftwareLicenseEditView,
|
||||||
SoftwareLicenseDeleteView,
|
SoftwareLicenseDeleteView,
|
||||||
SoftwareLicenseBulkImportView,
|
|
||||||
SoftwareLicenseBulkEditView,
|
|
||||||
SoftwareLicenseBulkDeleteView,
|
SoftwareLicenseBulkDeleteView,
|
||||||
|
SoftwareLicenseBulkImportView,
|
||||||
)
|
)
|
||||||
from .software_product import (
|
from .software_product import (
|
||||||
SoftwareProductListView,
|
SoftwareProductListView,
|
||||||
SoftwareProductView,
|
SoftwareProductView,
|
||||||
SoftwareProductEditView,
|
SoftwareProductEditView,
|
||||||
SoftwareProductDeleteView,
|
SoftwareProductDeleteView,
|
||||||
SoftwareProductBulkImportView,
|
|
||||||
SoftwareProductBulkEditView,
|
|
||||||
SoftwareProductBulkDeleteView,
|
SoftwareProductBulkDeleteView,
|
||||||
|
SoftwareProductBulkImportView,
|
||||||
)
|
)
|
||||||
from .software_product_installation import (
|
from .software_product_installation import (
|
||||||
SoftwareProductInstallationListView,
|
SoftwareProductInstallationListView,
|
||||||
SoftwareProductInstallationView,
|
SoftwareProductInstallationView,
|
||||||
SoftwareProductInstallationEditView,
|
SoftwareProductInstallationEditView,
|
||||||
SoftwareProductInstallationDeleteView,
|
SoftwareProductInstallationDeleteView,
|
||||||
SoftwareProductInstallationBulkImportView,
|
|
||||||
SoftwareProductInstallationBulkEditView,
|
|
||||||
SoftwareProductInstallationBulkDeleteView,
|
SoftwareProductInstallationBulkDeleteView,
|
||||||
|
SoftwareProductInstallationBulkImportView,
|
||||||
)
|
)
|
||||||
from .software_product_version import (
|
from .software_product_version import (
|
||||||
SoftwareProductVersionListView,
|
SoftwareProductVersionListView,
|
||||||
SoftwareProductVersionView,
|
SoftwareProductVersionView,
|
||||||
SoftwareProductVersionEditView,
|
SoftwareProductVersionEditView,
|
||||||
SoftwareProductVersionDeleteView,
|
SoftwareProductVersionDeleteView,
|
||||||
SoftwareProductVersionBulkImportView,
|
|
||||||
SoftwareProductVersionBulkEditView,
|
|
||||||
SoftwareProductVersionBulkDeleteView,
|
SoftwareProductVersionBulkDeleteView,
|
||||||
|
SoftwareProductVersionBulkImportView,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,49 +1,75 @@
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from netbox_slm import filtersets, forms, tables
|
from netbox_slm import filtersets, forms, tables
|
||||||
from netbox_slm.models import SoftwareLicense
|
from netbox_slm.models import SoftwareLicense
|
||||||
|
from utilities.views import register_model_view
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNotificationPermissionMixin:
|
||||||
|
"""Prevent users without the dedicated permission from changing SMTP delivery."""
|
||||||
|
|
||||||
|
email_permission = "netbox_slm.manage_softwarelicense_email_notifications"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
form_attribute = "model_form" if hasattr(self, "model_form") else "form"
|
||||||
|
base_form = getattr(self, form_attribute)
|
||||||
|
can_manage_email = request.user.has_perm(self.email_permission)
|
||||||
|
|
||||||
|
class PermissionAwareForm(base_form):
|
||||||
|
def __init__(self, *form_args, **form_kwargs):
|
||||||
|
super().__init__(*form_args, **form_kwargs)
|
||||||
|
if not can_manage_email and "email_notifications" in self.fields:
|
||||||
|
field = self.fields["email_notifications"]
|
||||||
|
field.disabled = True
|
||||||
|
field.help_text = _(
|
||||||
|
"Only administrators or explicitly authorized users can change this setting."
|
||||||
|
)
|
||||||
|
|
||||||
|
setattr(self, form_attribute, PermissionAwareForm)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareLicense, "list", path="", detail=False)
|
||||||
class SoftwareLicenseListView(generic.ObjectListView):
|
class SoftwareLicenseListView(generic.ObjectListView):
|
||||||
"""View for listing all existing SoftwareLicenses."""
|
|
||||||
|
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
filterset = filtersets.SoftwareLicenseFilterSet
|
filterset = filtersets.SoftwareLicenseFilterSet
|
||||||
filterset_form = forms.SoftwareLicenseFilterForm
|
filterset_form = forms.SoftwareLicenseFilterForm
|
||||||
table = tables.SoftwareLicenseTable
|
table = tables.SoftwareLicenseTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareLicense)
|
||||||
class SoftwareLicenseView(generic.ObjectView):
|
class SoftwareLicenseView(generic.ObjectView):
|
||||||
"""Display SoftwareLicense details"""
|
|
||||||
|
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseEditView(generic.ObjectEditView):
|
@register_model_view(SoftwareLicense, "add", detail=False)
|
||||||
"""View for editing and creating a SoftwareLicense instance."""
|
@register_model_view(SoftwareLicense, "edit")
|
||||||
|
class SoftwareLicenseEditView(EmailNotificationPermissionMixin, generic.ObjectEditView):
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
form = forms.SoftwareLicenseForm
|
form = forms.SoftwareLicenseForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareLicense, "delete")
|
||||||
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
||||||
"""View for deleting a SoftwareLicense instance"""
|
|
||||||
|
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseBulkImportView(generic.BulkImportView):
|
@register_model_view(SoftwareLicense, "bulk_import", detail=False)
|
||||||
|
class SoftwareLicenseBulkImportView(EmailNotificationPermissionMixin, generic.BulkImportView):
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
model_form = forms.SoftwareLicenseImportForm
|
model_form = forms.SoftwareLicenseBulkImportForm
|
||||||
table = tables.SoftwareLicenseTable
|
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLicenseBulkEditView(generic.BulkEditView):
|
@register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False)
|
||||||
|
class SoftwareLicenseBulkEditView(EmailNotificationPermissionMixin, generic.BulkEditView):
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
filterset = filtersets.SoftwareLicenseFilterSet
|
filterset = filtersets.SoftwareLicenseFilterSet
|
||||||
table = tables.SoftwareLicenseTable
|
table = tables.SoftwareLicenseTable
|
||||||
form = forms.SoftwareLicenseBulkEditForm
|
form = forms.SoftwareLicenseBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareLicense, "bulk_delete", path="delete", detail=False)
|
||||||
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SoftwareLicense.objects.all()
|
queryset = SoftwareLicense.objects.all()
|
||||||
|
filterset = filtersets.SoftwareLicenseFilterSet
|
||||||
table = tables.SoftwareLicenseTable
|
table = tables.SoftwareLicenseTable
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox_slm import filtersets, forms, tables
|
from netbox_slm import filtersets, forms, tables
|
||||||
from netbox_slm.models import SoftwareProduct
|
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):
|
class SoftwareProductListView(generic.ObjectListView):
|
||||||
"""View for listing all existing SoftwareProducts."""
|
|
||||||
|
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
filterset = filtersets.SoftwareProductFilterSet
|
filterset = filtersets.SoftwareProductFilterSet
|
||||||
filterset_form = forms.SoftwareProductFilterForm
|
filterset_form = forms.SoftwareProductFilterForm
|
||||||
table = tables.SoftwareProductTable
|
table = tables.SoftwareProductTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct)
|
||||||
class SoftwareProductView(generic.ObjectView):
|
class SoftwareProductView(generic.ObjectView):
|
||||||
"""Display SoftwareProduct details"""
|
|
||||||
|
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
@@ -22,33 +21,34 @@ class SoftwareProductView(generic.ObjectView):
|
|||||||
return {"versions": versions}
|
return {"versions": versions}
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct, "add", detail=False)
|
||||||
|
@register_model_view(SoftwareProduct, "edit")
|
||||||
class SoftwareProductEditView(generic.ObjectEditView):
|
class SoftwareProductEditView(generic.ObjectEditView):
|
||||||
"""View for editing and creating a SoftwareProduct instance."""
|
|
||||||
|
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
form = forms.SoftwareProductForm
|
form = forms.SoftwareProductForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct, "delete")
|
||||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||||
"""View for deleting a SoftwareProduct instance"""
|
|
||||||
|
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct, "bulk_import", detail=False)
|
||||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
model_form = forms.SoftwareProductImportForm
|
model_form = forms.SoftwareProductBulkImportForm
|
||||||
table = tables.SoftwareProductTable
|
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct, "bulk_edit", path="edit", detail=False)
|
||||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
filterset = filtersets.SoftwareProductFilterSet
|
filterset = filtersets.SoftwareProductFilterSet
|
||||||
filterset_form = forms.SoftwareProductFilterForm
|
|
||||||
table = tables.SoftwareProductTable
|
table = tables.SoftwareProductTable
|
||||||
form = forms.SoftwareProductBulkEditForm
|
form = forms.SoftwareProductBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProduct, "bulk_delete", path="delete", detail=False)
|
||||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SoftwareProduct.objects.all()
|
queryset = SoftwareProduct.objects.all()
|
||||||
|
filterset = filtersets.SoftwareProductFilterSet
|
||||||
table = tables.SoftwareProductTable
|
table = tables.SoftwareProductTable
|
||||||
|
|||||||
@@ -1,42 +1,41 @@
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox_slm import filtersets, forms, tables
|
from netbox_slm import filtersets, forms, tables
|
||||||
from netbox_slm.models import SoftwareProductInstallation
|
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):
|
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||||
"""View for listing all existing SoftwareProductInstallations."""
|
|
||||||
|
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||||
table = tables.SoftwareProductInstallationTable
|
table = tables.SoftwareProductInstallationTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation)
|
||||||
class SoftwareProductInstallationView(generic.ObjectView):
|
class SoftwareProductInstallationView(generic.ObjectView):
|
||||||
"""Display SoftwareProductInstallation details"""
|
|
||||||
|
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation, "add", detail=False)
|
||||||
|
@register_model_view(SoftwareProductInstallation, "edit")
|
||||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
|
||||||
|
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
form = forms.SoftwareProductInstallationForm
|
form = forms.SoftwareProductInstallationForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation, "delete")
|
||||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||||
"""View for deleting a SoftwareProductInstallation instance"""
|
|
||||||
|
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation, "bulk_import", detail=False)
|
||||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
model_form = forms.SoftwareProductInstallationImportForm
|
model_form = forms.SoftwareProductInstallationBulkImportForm
|
||||||
table = tables.SoftwareProductInstallationTable
|
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation, "bulk_edit", path="edit", detail=False)
|
||||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||||
@@ -44,6 +43,8 @@ class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.SoftwareProductInstallationBulkEditForm
|
form = forms.SoftwareProductInstallationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductInstallation, "bulk_delete", path="delete", detail=False)
|
||||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SoftwareProductInstallation.objects.all()
|
queryset = SoftwareProductInstallation.objects.all()
|
||||||
|
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||||
table = tables.SoftwareProductInstallationTable
|
table = tables.SoftwareProductInstallationTable
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox_slm import filtersets, forms, tables
|
from netbox_slm import filtersets, forms, tables
|
||||||
from netbox_slm.models import SoftwareProductVersion
|
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):
|
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||||
"""View for listing all existing SoftwareProductVersions."""
|
|
||||||
|
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||||
table = tables.SoftwareProductVersionTable
|
table = tables.SoftwareProductVersionTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion)
|
||||||
class SoftwareProductVersionView(generic.ObjectView):
|
class SoftwareProductVersionView(generic.ObjectView):
|
||||||
"""Display SoftwareProductVersion details"""
|
|
||||||
|
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
@@ -22,25 +21,25 @@ class SoftwareProductVersionView(generic.ObjectView):
|
|||||||
return {"installations": installation_count}
|
return {"installations": installation_count}
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion, "add", detail=False)
|
||||||
|
@register_model_view(SoftwareProductVersion, "edit")
|
||||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
|
||||||
|
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
form = forms.SoftwareProductVersionForm
|
form = forms.SoftwareProductVersionForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion, "delete")
|
||||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||||
"""View for deleting a SoftwareProductVersion instance"""
|
|
||||||
|
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion, "bulk_import", detail=False)
|
||||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
model_form = forms.SoftwareProductVersionImportForm
|
model_form = forms.SoftwareProductVersionBulkImportForm
|
||||||
table = tables.SoftwareProductVersionTable
|
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion, "bulk_edit", path="edit", detail=False)
|
||||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||||
@@ -48,6 +47,8 @@ class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.SoftwareProductVersionBulkEditForm
|
form = forms.SoftwareProductVersionBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SoftwareProductVersion, "bulk_delete", path="delete", detail=False)
|
||||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SoftwareProductVersion.objects.all()
|
queryset = SoftwareProductVersion.objects.all()
|
||||||
|
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||||
table = tables.SoftwareProductVersionTable
|
table = tables.SoftwareProductVersionTable
|
||||||
|
|||||||
+36
-25
@@ -7,63 +7,71 @@ name = "netbox-slm"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Software Lifecycle Management Netbox Plugin"
|
description = "Software Lifecycle Management Netbox Plugin"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.11"
|
||||||
license = { text = "Apache-2.0" }
|
license = { text = "Apache-2.0" }
|
||||||
authors = [
|
authors = [
|
||||||
{name = "ICTU", email = "open-source-projects@ictu.nl"},
|
{name = "ICTU", email = "open-source-projects@ictu.nl"},
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Environment :: Plugins",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: System Administrators",
|
||||||
"License :: OSI Approved :: Apache Software License",
|
"License :: OSI Approved :: Apache Software License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
|
"Programming Language :: Python",
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||||
]
|
]
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"license-expression == 30.4.4",
|
||||||
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
build = [
|
build = [
|
||||||
"build == 1.0.3",
|
"build == 1.5.0",
|
||||||
"setuptools == 68.2.0",
|
"setuptools == 82.0.1",
|
||||||
"twine == 3.7.1",
|
"twine == 6.2.0",
|
||||||
]
|
]
|
||||||
tools = [
|
ci = [
|
||||||
"black == 23.3.0",
|
"coverage == 7.14.3",
|
||||||
"ruff == 0.0.275",
|
"ruff == 0.15.19",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/ICTU/netbox_slm/"
|
||||||
|
Changelog = "https://github.com/ICTU/netbox_slm/blob/master/CHANGELOG.md"
|
||||||
|
Issues = "https://github.com/ICTU/netbox_slm/issues/"
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "netbox_slm.__version__"}
|
version = {attr = "netbox_slm.__version__"}
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["netbox_slm"]
|
include = ["netbox_slm"]
|
||||||
|
|
||||||
[tool.black]
|
|
||||||
line-length = 120
|
|
||||||
target-version = ["py39"]
|
|
||||||
color = true
|
|
||||||
check = true
|
|
||||||
diff = true
|
|
||||||
fast = true
|
|
||||||
extend-exclude = '''
|
|
||||||
netbox_slm/migrations/.*.py
|
|
||||||
'''
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
target-version = "py39"
|
target-version = "py312"
|
||||||
format = "github"
|
|
||||||
src = ["netbox_slm"]
|
src = ["netbox_slm"]
|
||||||
|
|
||||||
[tool.ruff.per-file-ignores]
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle
|
||||||
|
"F", # Pyflakes
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"__init__.py" = [
|
"__init__.py" = [
|
||||||
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files
|
"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
|
"F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files
|
||||||
@@ -71,3 +79,6 @@ src = ["netbox_slm"]
|
|||||||
"netbox_slm/migrations/*.py" = [
|
"netbox_slm/migrations/*.py" = [
|
||||||
"E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files
|
"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"]
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
sonar.organization=ictu
|
sonar.organization=ictu
|
||||||
sonar.projectKey=ICTU_netbox_slm
|
sonar.projectKey=ICTU_netbox_slm
|
||||||
sonar.projectName=netbox-slm
|
sonar.projectName=netbox-slm
|
||||||
sonar.projectVersion=1.4
|
sonar.projectVersion=1.9.0
|
||||||
|
|
||||||
# Path is relative to the sonar-project.properties file
|
# Path is relative to the sonar-project.properties file
|
||||||
sonar.sources=netbox_slm
|
sonar.sources=netbox_slm
|
||||||
sonar.python.version=3.9, 3.10, 3.11
|
sonar.exclusions=netbox_slm/tests/**
|
||||||
|
sonar.tests=netbox_slm/tests
|
||||||
|
sonar.python.version=3.11, 3.12, 3.13, 3.14
|
||||||
|
|
||||||
# Unit tests
|
# Python test reports
|
||||||
sonar.python.xunit.reportPath=ci/reports/junit.xml
|
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
|
||||||
sonar.python.coverage.reportPaths=ci/reports/coverage.xml
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
git clone -b release https://github.com/netbox-community/netbox-docker.git
|
|
||||||
cd netbox-docker
|
|
||||||
tee docker-compose.override.yml <<EOF
|
|
||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
netbox:
|
|
||||||
ports:
|
|
||||||
- 8000:8080
|
|
||||||
postgres:
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
redis:
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
EOF
|
|
||||||
docker-compose pull
|
|
||||||
docker-compose up
|
|
||||||
Reference in New Issue
Block a user