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 | ||
|
|
1411e3a5cb | ||
|
|
d082af4a9b | ||
|
|
2501e41b7c | ||
|
|
aa41f8a402 | ||
|
|
6c512e0e9b | ||
|
|
5b96496b46 | ||
|
|
88edcde8d6 | ||
|
|
7f18a3ad6f | ||
|
|
1925be271a | ||
|
|
a53486ca23 | ||
|
|
4bc418a5d0 | ||
|
|
e952111a53 | ||
|
|
e73cb5fa1a | ||
|
|
56552bdd02 | ||
|
|
f914e78812 | ||
|
|
c9a7b2315e | ||
|
|
a8c9fc5fbe | ||
|
|
a1f6e3dc92 | ||
|
|
4f8053c0f6 | ||
|
|
3b3a6d63d4 | ||
|
|
ce283dea52 | ||
|
|
f3579cce63 | ||
|
|
3bb711ea84 | ||
|
|
9d76bb421d | ||
|
|
7a24221444 | ||
|
|
ac198ef7cf | ||
|
|
6d05b1b6ca | ||
|
|
049fb045c0 |
@@ -0,0 +1,3 @@
|
||||
COMPOSE_PATH_SEPARATOR=:
|
||||
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME=netbox-docker
|
||||
@@ -1,3 +1,4 @@
|
||||
**/netbox
|
||||
**/netbox-docker
|
||||
**/*.pyc
|
||||
**/__pycache__
|
||||
@@ -5,3 +6,8 @@
|
||||
**/.vscode
|
||||
**/dist
|
||||
**/netbox_slm.egg-info
|
||||
ci/docker-compose.override.yml
|
||||
ci/reports
|
||||
out/production
|
||||
*.iml
|
||||
build/
|
||||
|
||||
+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");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
1. Definitions.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include SECURITY.md
|
||||
graft netbox_slm
|
||||
prune netbox_slm/tests
|
||||
global-exclude *.py[cod]
|
||||
global-exclude __pycache__
|
||||
@@ -1,12 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
twine = "3.7.1"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
Generated
-192
@@ -1,192 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f3cebe84c169ba6773cfcfae65b47e5076fa331e445f47a6ec4d13306942deb4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
|
||||
"sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
|
||||
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.10"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
|
||||
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.18.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
|
||||
"sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.10.0"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
|
||||
"sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.5.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
|
||||
"sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
|
||||
],
|
||||
"version": "==1.8.2"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4",
|
||||
"sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.11.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
|
||||
"sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.6"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
|
||||
"sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==32.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5",
|
||||
"sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.27.0"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c",
|
||||
"sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.62.3"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
|
||||
"sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.7"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
|
||||
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
# NetBox SLM
|
||||
|
||||
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.
|
||||
|
||||
Diese Variante ist für **NetBox 4.6.5** vorgesehen.
|
||||
|
||||
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.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Softwareprodukte und Hersteller verwalten
|
||||
- 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
|
||||
|
||||
> **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.
|
||||
|
||||
## Installation
|
||||
|
||||
NetBox installiert lokale Erweiterungen aus `/opt/netbox/local_requirements.txt`. Die Git-URL muss dort dauerhaft
|
||||
eingetragen werden, damit das Plugin bei einem späteren NetBox-Upgrade erneut installiert wird.
|
||||
|
||||
### 1. Git-Repository als lokale Anforderung eintragen
|
||||
|
||||
```bash
|
||||
echo "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" >> /opt/netbox/local_requirements.txt
|
||||
```
|
||||
|
||||
Vorher kann geprüft werden, ob der Eintrag bereits vorhanden ist:
|
||||
|
||||
```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
|
||||
|
||||
Die Installation muss im Python-Virtual-Environment von NetBox erfolgen:
|
||||
|
||||
```bash
|
||||
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
||||
"git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git"
|
||||
```
|
||||
|
||||
Alternativ können alle lokalen Anforderungen installiert werden:
|
||||
|
||||
```bash
|
||||
/opt/netbox/venv/bin/pip install -r /opt/netbox/local_requirements.txt
|
||||
```
|
||||
|
||||
Die installierte Version kann anschließend geprüft werden:
|
||||
|
||||
```bash
|
||||
/opt/netbox/venv/bin/pip show netbox-slm
|
||||
```
|
||||
|
||||
Für diese Variante muss dort mindestens Version `1.12.0` stehen.
|
||||
|
||||
## Ablaufbenachrichtigungen
|
||||
|
||||
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.
|
||||
|
||||
Empfänger entscheiden selbst, ob sie informiert werden: Vertrieb, zuständiger Techniker oder andere Benutzer öffnen
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Die Aktivierung des E-Mail-Versands darf nur durch Administratoren oder Benutzer mit der Berechtigung
|
||||
`netbox_slm.manage_softwarelicense_email_notifications` geändert werden. Die eigentlichen SMTP-Zugangsdaten werden
|
||||
ausschließlich in der NetBox-Serverkonfiguration gepflegt und sind im Plugin nicht einsehbar.
|
||||
|
||||
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.
|
||||
|
||||
### 3. Plugin in NetBox aktivieren
|
||||
|
||||
In `/opt/netbox/netbox/netbox/configuration.py` ergänzen:
|
||||
|
||||
```python
|
||||
PLUGINS = [
|
||||
"netbox_slm",
|
||||
]
|
||||
```
|
||||
|
||||
Falls bereits andere Plugins eingetragen sind, wird `"netbox_slm"` lediglich zur vorhandenen Liste hinzugefügt.
|
||||
Eine zusätzliche `PLUGINS_CONFIG`-Konfiguration ist für den Standardbetrieb nicht erforderlich.
|
||||
|
||||
### 4. Datenbank migrieren und statische Dateien aktualisieren
|
||||
|
||||
```bash
|
||||
cd /opt/netbox/netbox
|
||||
/opt/netbox/venv/bin/python manage.py migrate
|
||||
/opt/netbox/venv/bin/python manage.py collectstatic --no-input
|
||||
```
|
||||
|
||||
### 5. NetBox-Dienste neu starten
|
||||
|
||||
Bei einer üblichen systemd-Installation:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
Je nach Installation können die Namen der Dienste abweichen.
|
||||
|
||||
## Aktualisierung
|
||||
|
||||
```bash
|
||||
/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
|
||||
```
|
||||
|
||||
Der Eintrag in `/opt/netbox/local_requirements.txt` bleibt dabei bestehen.
|
||||
|
||||
## Deinstallation
|
||||
|
||||
1. `"netbox_slm"` aus der `PLUGINS`-Liste entfernen.
|
||||
2. Die Git-Zeile aus `/opt/netbox/local_requirements.txt` entfernen.
|
||||
3. Das Python-Paket deinstallieren:
|
||||
|
||||
```bash
|
||||
/opt/netbox/venv/bin/pip uninstall netbox-slm
|
||||
```
|
||||
|
||||
Die Plugin-Tabellen werden dabei nicht automatisch aus der Datenbank gelöscht.
|
||||
|
||||
## 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.
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
Software Lifecycle Management
|
||||
-----------------------------
|
||||
|
||||
*Netbox Plugin for lifecycle management of software components, including versions and installations*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
--------------
|
||||
|
||||
Installation Guide
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
WARNING: This plugin is only tested with Netbox 3.2 at this time.
|
||||
|
||||
When using the Docker version of Netbox, first follow the `netbox-docker
|
||||
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
|
||||
instructions to clone the netbox-docker repo and set up the
|
||||
``docker-compose.override.yml``.
|
||||
|
||||
Next, follow these instructions (based on the `Netbox docker variant
|
||||
instructions <https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files>`__)
|
||||
to install the Netbox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/extra.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.97`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM netboxcommunity/netbox:v3.2.X
|
||||
|
||||
COPY ./plugin_requirements.txt /
|
||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
||||
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
version: '3.4'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- 8000:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-SLM
|
||||
image: netbox:slm
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
|
||||
Now, build the image: ``docker compose build --no-cache``
|
||||
|
||||
And finally, run Netbox with the SLM plugin: ``docker compose up``
|
||||
|
||||
Releasing Guide
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
To draft a release;
|
||||
|
||||
update the setup.py file to reflect the new version, then from the *src*
|
||||
directory run
|
||||
|
||||
::
|
||||
|
||||
# make sure to update the version in netbox_slm/__init__.py
|
||||
$ python setup.py sdist
|
||||
$ twine upload dist/*
|
||||
|
||||
On Github.com create a similar tag and version. These steps could be
|
||||
automated with a github workflow.
|
||||
|
||||
n.b. Currently the plugin is configured to use a personal pypi account,
|
||||
this should be changed.
|
||||
|
||||
Developer Guide (local installation)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Follow the steps below on your local system to run netbox and the
|
||||
netbox_slm plugin in developer mode*
|
||||
|
||||
Setup
|
||||
~~~~~
|
||||
|
||||
The goal below is to run all Netbox components in Docker and run a local
|
||||
Netbox Django copy with auto-reload to develop the plugin pointing to
|
||||
the Dockerized postgres and redis instances, basically ignoring the
|
||||
netbox docker runtime server.
|
||||
|
||||
Steps
|
||||
~~~~~
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox repository
|
||||
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
|
||||
install the virtual environment
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
|
||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
||||
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
|
||||
The Netbox installation above will be used to run Django management
|
||||
commands like runserver, makemigrations and migrate, which will be
|
||||
explained in the next steps below;
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox-slm repository
|
||||
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
|
||||
This will start Netbox locally (requires Docker) and forward the redis
|
||||
and postgres ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
|
||||
or stop the stack with
|
||||
|
||||
$ docker-compose down
|
||||
|
||||
# to start fresh:
|
||||
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and
|
||||
redis connection strings (username, password) to the ones the netbox
|
||||
docker backend is using, for example (using default user and passwords
|
||||
from the netbox docker example):
|
||||
|
||||
::
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox-slm/src/
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Copyright 2022-2025 ICTU
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
__version__ = "1.13.0"
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = "netbox_slm"
|
||||
verbose_name = _("Licences")
|
||||
version = __version__
|
||||
description = _("Licence Management NetBox Plugin.")
|
||||
author = "ICTU"
|
||||
author_email = "open-source-projects@ictu.nl"
|
||||
base_url = "slm"
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
"top_level_menu": True,
|
||||
"link_cluster_installations": "right",
|
||||
"link_device_installations": "right",
|
||||
"link_virtualmachine_installations": "right",
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
if settings.DEBUG:
|
||||
for model in apps.get_app_config("netbox_slm").get_models():
|
||||
admin.site.register(model)
|
||||
@@ -0,0 +1,187 @@
|
||||
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_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):
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"tags",
|
||||
"comments",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url", "name", "description")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
"comments",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
display = SerializerMethodField()
|
||||
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"id",
|
||||
"display",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"file_link",
|
||||
"release_type",
|
||||
"software_product",
|
||||
"tags",
|
||||
"comments",
|
||||
"custom_field_data",
|
||||
"created",
|
||||
"last_updated",
|
||||
)
|
||||
brief_fields = ("id", "display", "url", "name", "description")
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
@@ -4,6 +4,7 @@ from netbox_slm.api.views import (
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
SoftwareProductInstallationViewSet,
|
||||
SoftwareLicenseViewSet,
|
||||
)
|
||||
|
||||
router = NetBoxRouter()
|
||||
@@ -12,4 +13,6 @@ router.APIRootView = NetboxSLMRootView
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,50 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox_slm.api.serializers import (
|
||||
SoftwareProductSerializer,
|
||||
SoftwareProductVersionSerializer,
|
||||
SoftwareProductInstallationSerializer,
|
||||
SoftwareLicenseSerializer,
|
||||
)
|
||||
from netbox_slm.filtersets import (
|
||||
SoftwareProductFilterSet,
|
||||
SoftwareProductVersionFilterSet,
|
||||
SoftwareProductInstallationFilterSet,
|
||||
SoftwareLicenseFilterSet,
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
)
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilterSet
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilterSet
|
||||
|
||||
|
||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all().order_by("id")
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilterSet
|
||||
|
||||
|
||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareLicense.objects.all().order_by("id")
|
||||
serializer_class = SoftwareLicenseSerializer
|
||||
filterset_class = SoftwareLicenseFilterSet
|
||||
@@ -0,0 +1 @@
|
||||
LICENSE_EXPIRING = "netbox_slm_license_expiring"
|
||||
@@ -0,0 +1,153 @@
|
||||
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_slm.models import (
|
||||
SoftwareProduct,
|
||||
SoftwareProductVersion,
|
||||
SoftwareProductInstallation,
|
||||
SoftwareLicense,
|
||||
SoftwareReleaseTypes,
|
||||
RenewalIntervalUnits,
|
||||
)
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
|
||||
manufacturer_id = ModelMultipleChoiceFilter(queryset=Manufacturer.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(manufacturer__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductVersion instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
filename = CharFilter(lookup_expr="icontains")
|
||||
|
||||
release_type = MultipleChoiceFilter(choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
manufacturer_id = ModelMultipleChoiceFilter(
|
||||
field_name="software_product__manufacturer",
|
||||
queryset=Manufacturer.objects.all(),
|
||||
)
|
||||
|
||||
software_product_id = ModelMultipleChoiceFilter(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
label=_("Software Product"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductInstallation instances."""
|
||||
|
||||
device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all())
|
||||
virtualmachine_id = ModelMultipleChoiceFilter(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
label=_("Virtual Machine"),
|
||||
)
|
||||
cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all())
|
||||
software_product_id = ModelMultipleChoiceFilter(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
label=_("Software Product"),
|
||||
)
|
||||
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(software_product__name__icontains=value)
|
||||
| Q(software_product__manufacturer__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareLicense instances."""
|
||||
|
||||
name = CharFilter(lookup_expr="icontains")
|
||||
description = CharFilter(lookup_expr="icontains")
|
||||
type = CharFilter(lookup_expr="icontains")
|
||||
spdx_expression = CharFilter(lookup_expr="icontains", label=_("SPDX expression"))
|
||||
stored_location = CharFilter(lookup_expr="icontains")
|
||||
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:
|
||||
model = SoftwareLicense
|
||||
fields = ("support", "lifetime", "expiration_notifications_enabled", "email_notifications")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
| Q(description__icontains=value)
|
||||
| Q(software_product__name__icontains=value)
|
||||
| Q(version__name__icontains=value)
|
||||
| Q(installation__device__name__icontains=value)
|
||||
| Q(installation__virtualmachine__name__icontains=value)
|
||||
| Q(installation__cluster__name__icontains=value)
|
||||
| Q(tenant_group__name__icontains=value)
|
||||
| Q(tenant__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -0,0 +1,24 @@
|
||||
from .software_license import (
|
||||
SoftwareLicenseForm,
|
||||
SoftwareLicenseFilterForm,
|
||||
SoftwareLicenseBulkImportForm,
|
||||
SoftwareLicenseBulkEditForm,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductForm,
|
||||
SoftwareProductFilterForm,
|
||||
SoftwareProductBulkImportForm,
|
||||
SoftwareProductBulkEditForm,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationForm,
|
||||
SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationBulkImportForm,
|
||||
SoftwareProductInstallationBulkEditForm,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionForm,
|
||||
SoftwareProductVersionFilterForm,
|
||||
SoftwareProductVersionBulkImportForm,
|
||||
SoftwareProductVersionBulkEditForm,
|
||||
)
|
||||
@@ -0,0 +1,344 @@
|
||||
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField, Textarea
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import (
|
||||
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
|
||||
|
||||
|
||||
class SoftwareLicenseForm(NetBoxModelForm):
|
||||
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)
|
||||
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(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
installation = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
||||
),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
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"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"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",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet(
|
||||
"name",
|
||||
"description",
|
||||
"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)
|
||||
|
||||
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 Meta:
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"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",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"stored_location_url",
|
||||
"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"),
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
from django.forms import CharField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("name", "description", "manufacturer_id"),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
name = CharField(required=False)
|
||||
description = CharField(required=False)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_("Manufacturer"),
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductBulkImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (FieldSet("manufacturer"),)
|
||||
nullable_fields = ("manufacturer", "comments")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
from django.forms import ValidationError
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelect
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
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,
|
||||
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(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
|
||||
def clean_version(self):
|
||||
version = self.cleaned_data["version"]
|
||||
software_product = self.cleaned_data["software_product"]
|
||||
if version not in software_product.softwareproductversion_set.all():
|
||||
raise ValidationError(
|
||||
_("Version '%(version)s' does not belong to %(product)s. Select a compatible version.")
|
||||
% {"version": version, "product": software_product}
|
||||
)
|
||||
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):
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("device_id", "virtualmachine_id", "cluster_id", "software_product_id", "version_id"),
|
||||
)
|
||||
selector_fields = ("filter_id", "q")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_("Device"),
|
||||
)
|
||||
virtualmachine_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label=_("Virtual Machine"),
|
||||
)
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label=_("Cluster"),
|
||||
)
|
||||
software_product_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
version_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
label=_("Version"),
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"device",
|
||||
"virtualmachine",
|
||||
"cluster",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (FieldSet("device", "virtualmachine", "cluster", "software_product", "version"),)
|
||||
nullable_fields = ("device", "virtualmachine", "cluster", "comments")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label=_("Virtual Machine"),
|
||||
)
|
||||
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
from django.forms import DateField, CharField, MultipleChoiceField
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
DynamicModelChoiceField,
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelect, DatePicker
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
release_date = DateField(required=False, widget=DatePicker())
|
||||
end_of_support = DateField(required=False, widget=DatePicker())
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
label=_("Software Product"),
|
||||
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"file_link",
|
||||
"release_type",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
FieldSet("q", "filter_id", "tag"),
|
||||
FieldSet("name", "description", "filename", "release_type", "manufacturer_id", "software_product_id"),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
name = CharField(required=False)
|
||||
description = CharField(required=False)
|
||||
filename = CharField(required=False)
|
||||
|
||||
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_("Manufacturer"),
|
||||
)
|
||||
software_product_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm):
|
||||
release_type = CharField(
|
||||
help_text=_("Release type (possible values: %(values)s)") % {"values": SoftwareReleaseTypes.values}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"release_type",
|
||||
"tags",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
"release_date",
|
||||
"documentation_url",
|
||||
"end_of_support",
|
||||
"filename",
|
||||
"file_checksum",
|
||||
"file_link",
|
||||
"release_type",
|
||||
"software_product",
|
||||
),
|
||||
)
|
||||
nullable_fields = ("release_date", "documentation_url", "end_of_support", "filename", "file_checksum", "file_link")
|
||||
|
||||
tag = TagFilterField(model)
|
||||
comments = CommentField()
|
||||
|
||||
release_date = DateField(required=False, widget=DatePicker())
|
||||
documentation_url = CharField(required=False)
|
||||
end_of_support = DateField(required=False, widget=DatePicker())
|
||||
filename = CharField(required=False)
|
||||
file_checksum = CharField(required=False)
|
||||
file_link = CharField(required=False)
|
||||
|
||||
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False,
|
||||
label=_("Software Product"),
|
||||
)
|
||||
@@ -0,0 +1,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."
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.2.9 on 2022-04-15 07:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0153_created_datetimefield'),
|
||||
('virtualization', '0029_created_datetimefield'),
|
||||
('netbox_slm', '0003_auto_20220407_1209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='version',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='virtualmachine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='software_product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.1.8 on 2023-06-27 08:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('netbox_slm', '0004_auto_20220415_0705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SoftwareLicense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('type', models.CharField(max_length=128)),
|
||||
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expiration_date', models.DateField(blank=True, null=True)),
|
||||
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
|
||||
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-22 00:34
|
||||
|
||||
from django.db import migrations
|
||||
import netbox_slm.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_slm', '0005_add_software_license'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwarelicense',
|
||||
name='stored_location_url',
|
||||
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-04 10:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0036_virtualmachine_config_template'),
|
||||
('netbox_slm', '0006_softwarelicense_stored_location_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwarelicense',
|
||||
name='installation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='softwareproductinstallation',
|
||||
constraint=models.CheckConstraint(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",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,355 @@
|
||||
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.urls import reverse
|
||||
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 utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import EnhancedURLValidator
|
||||
|
||||
spdx_licensing: Licensing = get_spdx_licensing()
|
||||
|
||||
|
||||
class LaxURLField(models.URLField):
|
||||
"""
|
||||
NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField
|
||||
Overriding default_validators is needed, as they are always added
|
||||
"""
|
||||
|
||||
default_validators = [EnhancedURLValidator()]
|
||||
|
||||
|
||||
class SoftwareProduct(NetBoxModel):
|
||||
name = models.CharField(_("name"), max_length=128)
|
||||
comments = models.TextField(_("comments"), 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()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("software product")
|
||||
verbose_name_plural = _("software products")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
search_target = reverse(
|
||||
"plugins:netbox_slm:softwareproductinstallation_list", query={"software_product_id": self.pk}
|
||||
)
|
||||
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||
|
||||
|
||||
class SoftwareReleaseTypes(models.TextChoices):
|
||||
ALPHA = "A", _("Alpha")
|
||||
BETA = "B", _("Beta")
|
||||
RELEASE_CANDIDATE = "RC", _("Release candidate")
|
||||
STABLE = "S", _("Stable release")
|
||||
|
||||
|
||||
class 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):
|
||||
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(
|
||||
to="netbox_slm.SoftwareProduct",
|
||||
verbose_name=_("software product"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("software product version")
|
||||
verbose_name_plural = _("software product versions")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list", query={"version_id": self.pk})
|
||||
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
|
||||
|
||||
|
||||
class SoftwareProductInstallation(NetBoxModel):
|
||||
comments = models.TextField(_("comments"), 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
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk} ({self.platform})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("software product installation")
|
||||
verbose_name_plural = _("software product installations")
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name="%(app_label)s_%(class)s_platform",
|
||||
condition=(
|
||||
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
|
||||
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
|
||||
),
|
||||
violation_error_message=_(
|
||||
"Select exactly one platform destination: device, virtual machine, or cluster."
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
return self.device or self.virtualmachine or self.cluster
|
||||
|
||||
def render_type(self):
|
||||
if self.device:
|
||||
return "device"
|
||||
if self.virtualmachine:
|
||||
return "virtualmachine"
|
||||
return "cluster"
|
||||
|
||||
|
||||
def spdx_license_names():
|
||||
names = [(item[0], item[0]) for item in spdx_licensing.known_symbols.items() if not item[1].is_exception]
|
||||
names.sort()
|
||||
names.insert(0, ("", "---------")) # set default value
|
||||
return names
|
||||
|
||||
|
||||
def validate_spdx_expression(value):
|
||||
expression_info = spdx_licensing.validate(value)
|
||||
if expression_info.errors:
|
||||
raise ValidationError(_("%(value)s is not a known SPDX license expression") % {"value": value})
|
||||
|
||||
|
||||
class SoftwareLicense(NetBoxModel):
|
||||
name = models.CharField(_("name"), max_length=128)
|
||||
comments = models.TextField(_("comments"), blank=True)
|
||||
|
||||
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
|
||||
type = models.CharField(_("type"), max_length=128, blank=True)
|
||||
spdx_expression = models.CharField(
|
||||
_("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", 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(
|
||||
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()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("software license")
|
||||
verbose_name_plural = _("software licenses")
|
||||
permissions = [
|
||||
("manage_softwarelicense_email_notifications", _("Can manage license email notifications")),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
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
|
||||
def stored_location_txt(self):
|
||||
if self.stored_location_url and not self.stored_location:
|
||||
return _("Link")
|
||||
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",
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from . import SLMConfig
|
||||
|
||||
slm_items = (
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwareproduct_list",
|
||||
link_text=_("Software Products"),
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_add",
|
||||
_("Add"),
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_bulk_import",
|
||||
_("Import"),
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproduct"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwareproductversion_list",
|
||||
link_text=_("Versions"),
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_add",
|
||||
_("Add"),
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_bulk_import",
|
||||
_("Import"),
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproductversion"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwareproductinstallation_list",
|
||||
link_text=_("Installations"),
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_add",
|
||||
_("Add"),
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
|
||||
_("Import"),
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwareproductinstallation"],
|
||||
),
|
||||
),
|
||||
),
|
||||
PluginMenuItem(
|
||||
link="plugins:netbox_slm:softwarelicense_list",
|
||||
link_text=_("Licenses"),
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_add",
|
||||
_("Add"),
|
||||
"mdi mdi-plus-thick",
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_bulk_import",
|
||||
_("Import"),
|
||||
"mdi mdi-upload",
|
||||
permissions=["netbox_slm.import_softwarelicense"],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if get_plugin_config("netbox_slm", "top_level_menu"):
|
||||
menu = PluginMenu(
|
||||
label=_("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
|
||||
@@ -0,0 +1,197 @@
|
||||
import django_tables2 as tables
|
||||
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_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count")
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
installations = tables.Column(accessor="get_installation_count")
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"release_date",
|
||||
"end_of_support",
|
||||
"release_type",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"release_date",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
|
||||
("-" if is_descending else "") + "count"
|
||||
)
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
platform = tables.Column(accessor="platform", linkify=True)
|
||||
type = tables.Column(accessor="render_type")
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_platform(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True
|
||||
|
||||
def order_type(self, queryset, is_descending):
|
||||
device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
|
||||
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine"))
|
||||
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster"))
|
||||
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
|
||||
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
|
||||
|
||||
|
||||
class SoftwareLicenseTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareLicense objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
type = tables.Column()
|
||||
spdx_expression = tables.Column(verbose_name=_("SPDX expression"))
|
||||
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
|
||||
|
||||
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
|
||||
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
installation = tables.Column(accessor="installation", linkify=True)
|
||||
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")
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"spdx_expression",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"lifetime",
|
||||
"renewal_interval",
|
||||
"renewal_interval_unit",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"support",
|
||||
"license_amount",
|
||||
"provider_portal_url",
|
||||
"expiration_notifications_enabled",
|
||||
"email_notifications",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"type",
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"expiration_date",
|
||||
"lifetime",
|
||||
"expiration_notifications_enabled",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def render_installation(self, **kwargs):
|
||||
return f"{kwargs['record'].installation.platform}"
|
||||
@@ -0,0 +1,36 @@
|
||||
from netbox.plugins import PluginTemplateExtension, get_plugin_config
|
||||
|
||||
installations = dict(
|
||||
device=get_plugin_config("netbox_slm", "link_device_installations"),
|
||||
cluster=get_plugin_config("netbox_slm", "link_cluster_installations"),
|
||||
virtualmachine=get_plugin_config("netbox_slm", "link_virtualmachine_installations"),
|
||||
)
|
||||
|
||||
|
||||
class InstallationsCard(PluginTemplateExtension):
|
||||
models = ["dcim.device", "virtualization.cluster", "virtualization.virtualmachine"]
|
||||
|
||||
def get_object_name(self):
|
||||
object_model_meta = self.context["object"]._meta # one of the self.models defined above
|
||||
return object_model_meta.model_name
|
||||
|
||||
def render_card(self):
|
||||
return self.render(
|
||||
"netbox_slm/installations_card_include.html",
|
||||
extra_context={
|
||||
"search_obj": self.get_object_name(),
|
||||
},
|
||||
)
|
||||
|
||||
def left_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "left" else ""
|
||||
|
||||
def right_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "right" else ""
|
||||
|
||||
def full_width_page(self):
|
||||
return self.render_card() if installations[self.get_object_name()] == "full" else ""
|
||||
|
||||
|
||||
# auto imported by default PluginConfig.template_extensions = template_content.template_extensions
|
||||
template_extensions = [InstallationsCard]
|
||||
@@ -0,0 +1,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>
|
||||
@@ -0,0 +1,165 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Software License" %}
|
||||
</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "SPDX expression" %}</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 %}
|
||||
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
|
||||
{% else %}
|
||||
<td>{{ object.stored_location_txt }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Start date" %}</th>
|
||||
<td>{{ object.start_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Expiration date" %}</th>
|
||||
<td>{{ object.expiration_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Lifetime license" %}</th>
|
||||
<td>{% if object.lifetime %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Renewal interval" %}</th>
|
||||
<td>
|
||||
{% if object.renewal_interval %}
|
||||
{{ object.renewal_interval }} {{ object.get_renewal_interval_unit_display }}
|
||||
{% else %}
|
||||
{{ None|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</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>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Software Product" %}
|
||||
</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Versions" %}</th>
|
||||
<td>
|
||||
{% for version in object.softwareproductversion_set.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
|
||||
{{ version }}
|
||||
</a>
|
||||
{% empty %}
|
||||
{% trans "None" %}
|
||||
{% endfor %}
|
||||
</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>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Software Product Installation" %}
|
||||
</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.device %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
{% elif object.virtualmachine %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Machine" %}</th>
|
||||
<td>{{ object.virtualmachine|linkify }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cluster" %}</th>
|
||||
<td>{{ object.cluster|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<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 "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>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Software Product Version" %}
|
||||
</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.software_product.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<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>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,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")
|
||||
@@ -0,0 +1,210 @@
|
||||
from datetime import date, timedelta
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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(SlmBaseTestCase):
|
||||
"""Test basic model functionality and custom overrides"""
|
||||
|
||||
def test_model_name(self):
|
||||
self.assertEqual(self.p_name, str(self.software_product))
|
||||
self.assertEqual(self.v_name, str(self.software_product_version))
|
||||
self.assertTrue(str(self.software_product_installation)[0].isdigit()) # starts with PK
|
||||
self.assertEqual(self.l_name, str(self.software_license))
|
||||
|
||||
def test_absolute_url(self):
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/software-products/{self.software_product.pk}/", self.software_product.get_absolute_url()
|
||||
)
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/versions/{self.software_product_version.pk}/",
|
||||
self.software_product_version.get_absolute_url(),
|
||||
)
|
||||
self.assertEqual(
|
||||
f"/plugins/slm/installations/{self.software_product_installation.pk}/",
|
||||
self.software_product_installation.get_absolute_url(),
|
||||
)
|
||||
self.assertEqual(f"/plugins/slm/licenses/{self.software_license.pk}/", self.software_license.get_absolute_url())
|
||||
|
||||
def test_get_installation_count(self):
|
||||
self.assertEqual(
|
||||
f"<a href='/plugins/slm/installations/?software_product_id={self.software_product.pk}'>1</a>",
|
||||
self.software_product.get_installation_count(),
|
||||
)
|
||||
self.assertEqual(
|
||||
f"<a href='/plugins/slm/installations/?version_id={self.software_product_version.pk}'>1</a>",
|
||||
self.software_product_version.get_installation_count(),
|
||||
)
|
||||
|
||||
def test_product_installation_methods(self):
|
||||
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.vm, self.software_product_installation.platform)
|
||||
|
||||
self.software_product_installation.virtualmachine = None
|
||||
self.software_product_installation.device = self.device
|
||||
self.software_product_installation.save()
|
||||
self.assertEqual("device", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.device, self.software_product_installation.platform)
|
||||
|
||||
self.software_product_installation.device = None
|
||||
self.software_product_installation.cluster = self.cluster
|
||||
self.software_product_installation.save()
|
||||
self.assertEqual("cluster", self.software_product_installation.render_type())
|
||||
self.assertEqual(self.cluster, self.software_product_installation.platform)
|
||||
|
||||
def test_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):
|
||||
self.assertEqual("Link", self.software_license.stored_location_txt)
|
||||
|
||||
self.software_license.stored_location = "GitHub"
|
||||
self.software_license.save()
|
||||
self.assertEqual("GitHub", self.software_license.stored_location_txt)
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.urls import path, include
|
||||
|
||||
from netbox_slm import views # noqa F401
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("software-products/", include(get_model_urls("netbox_slm", "softwareproduct", detail=False))),
|
||||
path("software-products/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproduct"))),
|
||||
path("versions/", include(get_model_urls("netbox_slm", "softwareproductversion", detail=False))),
|
||||
path("versions/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductversion"))),
|
||||
path("installations/", include(get_model_urls("netbox_slm", "softwareproductinstallation", detail=False))),
|
||||
path("installations/<int:pk>/", include(get_model_urls("netbox_slm", "softwareproductinstallation"))),
|
||||
path("licenses/", include(get_model_urls("netbox_slm", "softwarelicense", detail=False))),
|
||||
path("licenses/<int:pk>/", include(get_model_urls("netbox_slm", "softwarelicense"))),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from .software_license import (
|
||||
SoftwareLicenseListView,
|
||||
SoftwareLicenseView,
|
||||
SoftwareLicenseEditView,
|
||||
SoftwareLicenseDeleteView,
|
||||
SoftwareLicenseBulkDeleteView,
|
||||
SoftwareLicenseBulkImportView,
|
||||
)
|
||||
from .software_product import (
|
||||
SoftwareProductListView,
|
||||
SoftwareProductView,
|
||||
SoftwareProductEditView,
|
||||
SoftwareProductDeleteView,
|
||||
SoftwareProductBulkDeleteView,
|
||||
SoftwareProductBulkImportView,
|
||||
)
|
||||
from .software_product_installation import (
|
||||
SoftwareProductInstallationListView,
|
||||
SoftwareProductInstallationView,
|
||||
SoftwareProductInstallationEditView,
|
||||
SoftwareProductInstallationDeleteView,
|
||||
SoftwareProductInstallationBulkDeleteView,
|
||||
SoftwareProductInstallationBulkImportView,
|
||||
)
|
||||
from .software_product_version import (
|
||||
SoftwareProductVersionListView,
|
||||
SoftwareProductVersionView,
|
||||
SoftwareProductVersionEditView,
|
||||
SoftwareProductVersionDeleteView,
|
||||
SoftwareProductVersionBulkDeleteView,
|
||||
SoftwareProductVersionBulkImportView,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
from netbox.views import generic
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
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):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
filterset_form = forms.SoftwareLicenseFilterForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense)
|
||||
class SoftwareLicenseView(generic.ObjectView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "add", detail=False)
|
||||
@register_model_view(SoftwareLicense, "edit")
|
||||
class SoftwareLicenseEditView(EmailNotificationPermissionMixin, generic.ObjectEditView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
form = forms.SoftwareLicenseForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "delete")
|
||||
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_import", detail=False)
|
||||
class SoftwareLicenseBulkImportView(EmailNotificationPermissionMixin, generic.BulkImportView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
model_form = forms.SoftwareLicenseBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareLicenseBulkEditView(EmailNotificationPermissionMixin, generic.BulkEditView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
form = forms.SoftwareLicenseBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareLicense, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
@@ -0,0 +1,54 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "list", path="", detail=False)
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct)
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproductversion_set.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "add", detail=False)
|
||||
@register_model_view(SoftwareProduct, "edit")
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = forms.SoftwareProductForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "delete")
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_import", detail=False)
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = forms.SoftwareProductBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
table = tables.SoftwareProductTable
|
||||
form = forms.SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProduct, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
table = tables.SoftwareProductTable
|
||||
@@ -0,0 +1,50 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductInstallation
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "list", path="", detail=False)
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation)
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "add", detail=False)
|
||||
@register_model_view(SoftwareProductInstallation, "edit")
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = forms.SoftwareProductInstallationForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "delete")
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_import", detail=False)
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = forms.SoftwareProductInstallationBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
form = forms.SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductInstallation, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
@@ -0,0 +1,54 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductVersion
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "list", path="", detail=False)
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion)
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "add", detail=False)
|
||||
@register_model_view(SoftwareProductVersion, "edit")
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = forms.SoftwareProductVersionForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "delete")
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_import", detail=False)
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = forms.SoftwareProductVersionBulkImportForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_edit", path="edit", detail=False)
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
form = forms.SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(SoftwareProductVersion, "bulk_delete", path="delete", detail=False)
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
@@ -0,0 +1,84 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "netbox-slm"
|
||||
dynamic = ["version"]
|
||||
description = "Software Lifecycle Management Netbox Plugin"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { text = "Apache-2.0" }
|
||||
authors = [
|
||||
{name = "ICTU", email = "open-source-projects@ictu.nl"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Plugins",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
]
|
||||
dependencies = [
|
||||
"license-expression == 30.4.4",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
build = [
|
||||
"build == 1.5.0",
|
||||
"setuptools == 82.0.1",
|
||||
"twine == 6.2.0",
|
||||
]
|
||||
ci = [
|
||||
"coverage == 7.14.3",
|
||||
"ruff == 0.15.19",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/ICTU/netbox_slm/"
|
||||
Changelog = "https://github.com/ICTU/netbox_slm/blob/master/CHANGELOG.md"
|
||||
Issues = "https://github.com/ICTU/netbox_slm/issues/"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "netbox_slm.__version__"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["netbox_slm"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
src = ["netbox_slm"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle
|
||||
"F", # Pyflakes
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"SIM", # flake8-simplify
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = [
|
||||
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files
|
||||
"F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files
|
||||
]
|
||||
"netbox_slm/migrations/*.py" = [
|
||||
"E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
exclude = ["netbox_slm/migrations/*.py"]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Project metadata
|
||||
sonar.organization=ictu
|
||||
sonar.projectKey=ICTU_netbox_slm
|
||||
sonar.projectName=netbox-slm
|
||||
sonar.projectVersion=1.9.0
|
||||
|
||||
# Path is relative to the sonar-project.properties file
|
||||
sonar.sources=netbox_slm
|
||||
sonar.exclusions=netbox_slm/tests/**
|
||||
sonar.tests=netbox_slm/tests
|
||||
sonar.python.version=3.11, 3.12, 3.13, 3.14
|
||||
|
||||
# Python test reports
|
||||
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
Copyright 2022 ICTU
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,3 +0,0 @@
|
||||
include ../LICENSE
|
||||
include ../README.rst
|
||||
recursive-include netbox_slm/templates *
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
Software Lifecycle Management
|
||||
-----------------------------
|
||||
|
||||
*Netbox Plugin for lifecycle management of software components, including versions and installations*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
--------------
|
||||
|
||||
Installation Guide
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using the Docker version of Netbox, first follow the `netbox-docker
|
||||
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
|
||||
instructions to clone the netbox-docker repo and set up the
|
||||
``docker-compose.override.yml``.
|
||||
|
||||
Next, follow these instructions (based on the `Netbox docker variant
|
||||
instructions <https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files>`__)
|
||||
to install the Netbox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/extra.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.94`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM netboxcommunity/netbox:v3.X.X
|
||||
|
||||
COPY ./plugin_requirements.txt /
|
||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
||||
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
version: '3.4'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- 8000:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-SLM
|
||||
image: netbox:slm
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
|
||||
Now, build the image: ``docker compose build --no-cache``
|
||||
|
||||
And finally, run Netbox with the SLM plugin: ``docker compose up``
|
||||
|
||||
Releasing Guide
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
To draft a release;
|
||||
|
||||
update the setup.py file to reflect the new version, then from the *src*
|
||||
directory run
|
||||
|
||||
::
|
||||
|
||||
# make sure to update the version in netbox_slm/__init__.py
|
||||
$ python setup.py sdist
|
||||
$ twine upload dist/*
|
||||
|
||||
On Github.com create a similar tag and version. These steps could be
|
||||
automated with a github workflow.
|
||||
|
||||
n.b. Currently the plugin is configured to use a personal pypi account,
|
||||
this should be changed.
|
||||
|
||||
Developer Guide (local installation)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Follow the steps below on your local system to run netbox and the
|
||||
netbox_slm plugin in developer mode*
|
||||
|
||||
Setup
|
||||
~~~~~
|
||||
|
||||
The goal below is to run all Netbox components in Docker and run a local
|
||||
Netbox Django copy with auto-reload to develop the plugin pointing to
|
||||
the Dockerized postgres and redis instances, basically ignoring the
|
||||
netbox docker runtime server.
|
||||
|
||||
Steps
|
||||
~~~~~
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox repository
|
||||
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
$ cd netbox
|
||||
|
||||
install the virtual environment
|
||||
|
||||
$ pipenv shell
|
||||
$ pipenv install
|
||||
|
||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
||||
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS = [
|
||||
'netbox_slm',
|
||||
]
|
||||
|
||||
The Netbox installation above will be used to run Django management
|
||||
commands like runserver, makemigrations and migrate, which will be
|
||||
explained in the next steps below;
|
||||
|
||||
::
|
||||
|
||||
from your projects directory clone the netbox-slm repository
|
||||
|
||||
$ git clone https://github.com/ICTU/netbox_slm
|
||||
$ cd netbox_slm
|
||||
$ ./start-netbox.sh
|
||||
|
||||
This will start Netbox locally (requires Docker) and forward the redis
|
||||
and postgres ports to the localhost (make sure there’s no processes
|
||||
using these ports or change the dockerfiles accordingly)
|
||||
|
||||
Note, you can also start and stop netbox by hand:
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox-docker
|
||||
$ docker-compose up -d
|
||||
|
||||
or stop the stack with
|
||||
|
||||
$ docker-compose down
|
||||
|
||||
# to start fresh:
|
||||
|
||||
$ docker-compose down
|
||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
||||
$ docker-compose up -d --force-recreate
|
||||
|
||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
||||
|
||||
Go back to the netbox configuration.py file and update the postgres and
|
||||
redis connection strings (username, password) to the ones the netbox
|
||||
docker backend is using, for example (using default user and passwords
|
||||
from the netbox docker example):
|
||||
|
||||
::
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
'CONN_MAX_AGE': 300, # Max database connection age
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
# Set this to True to skip TLS certificate verification
|
||||
# This can expose the connection to attacks, be careful
|
||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
||||
}
|
||||
}
|
||||
|
||||
<<collapsed>>
|
||||
|
||||
Now you can run commands from the netbox repository like this;
|
||||
|
||||
::
|
||||
|
||||
$ cd netbox/netbox
|
||||
$ export PYTHONPATH=../../netbox-slm/src/
|
||||
$ python3 manage.py migrate netbox_slm
|
||||
$ python3 manage.py runserver 8001
|
||||
|
||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
||||
@@ -1,17 +0,0 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class SLMConfig(PluginConfig):
|
||||
name = 'netbox_slm'
|
||||
verbose_name = 'Software Lifecycle Management'
|
||||
description = 'Software Lifecycle Management'
|
||||
version = '0.97'
|
||||
author = 'Hedde van der Heide'
|
||||
author_email = 'hedde.vanderheide@ictu.nl'
|
||||
base_url = 'slm'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'version_info': False
|
||||
}
|
||||
|
||||
config = SLMConfig
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductVersionInline(admin.TabularInline):
|
||||
model = SoftwareProductVersion
|
||||
fields = ('name',)
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(SoftwareProduct)
|
||||
class SoftwareProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
inlines = (SoftwareProductVersionInline,)
|
||||
@@ -1,55 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj.manufacturer.name} - {obj.name}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj.name
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = [
|
||||
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version', 'tags',
|
||||
'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj
|
||||
@@ -1,42 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox_slm.api.serializers import (
|
||||
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
|
||||
)
|
||||
from netbox_slm.filters import (
|
||||
SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter,
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
|
||||
)
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
"""
|
||||
NetboxSLM API root view
|
||||
"""
|
||||
|
||||
def get_view_name(self):
|
||||
return "NetboxSLM"
|
||||
|
||||
|
||||
class SoftwareProductViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
serializer_class = SoftwareProductSerializer
|
||||
filterset_class = SoftwareProductFilter
|
||||
|
||||
|
||||
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
serializer_class = SoftwareProductVersionSerializer
|
||||
filterset_class = SoftwareProductVersionFilter
|
||||
|
||||
|
||||
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilter
|
||||
@@ -1,50 +0,0 @@
|
||||
from django.forms import BoundField
|
||||
from django.urls import reverse
|
||||
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class CustomDynamicModelMultipleChoiceField(DynamicModelMultipleChoiceField):
|
||||
def get_bound_field(self, form, field_name):
|
||||
bound_field = BoundField(form, self, field_name)
|
||||
|
||||
# Set initial value based on prescribed child fields (if not already set)
|
||||
if not self.initial and self.initial_params:
|
||||
filter_kwargs = {}
|
||||
for kwarg, child_field in self.initial_params.items():
|
||||
value = form.initial.get(child_field.lstrip("$"))
|
||||
if value:
|
||||
filter_kwargs[kwarg] = value
|
||||
if filter_kwargs:
|
||||
self.initial = self.queryset.filter(**filter_kwargs).first()
|
||||
|
||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||
# will be populated on-demand via the APISelect widget.
|
||||
data = bound_field.value()
|
||||
|
||||
if data:
|
||||
field_name = getattr(self, "to_field_name") or "pk"
|
||||
filter = self.filter(field_name=field_name)
|
||||
try:
|
||||
self.queryset = filter.filter(self.queryset, data)
|
||||
except (TypeError, ValueError):
|
||||
# Catch any error caused by invalid initial data passed from the user
|
||||
self.queryset = self.queryset.none()
|
||||
else:
|
||||
self.queryset = self.queryset.none()
|
||||
|
||||
# Set the data URL on the APISelect widget (if not already set)
|
||||
widget = bound_field.field.widget
|
||||
if not widget.attrs.get("data-url"):
|
||||
app_label = self.queryset.model._meta.app_label
|
||||
model_name = self.queryset.model._meta.model_name
|
||||
|
||||
#
|
||||
# Custom url for work with plugin api
|
||||
#
|
||||
data_url = reverse(
|
||||
"plugins-api:{}-api:{}-list".format(app_label, model_name)
|
||||
)
|
||||
widget.attrs["data-url"] = data_url
|
||||
|
||||
return bound_field
|
||||
@@ -1,75 +0,0 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class BaseFilter(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductFilter(BaseFilter):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",) # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilter(BaseFilter):
|
||||
name = django_filters.CharFilter(
|
||||
lookup_expr="icontains", label="Version"
|
||||
)
|
||||
# software_product_id = django_filters.ModelMultipleChoiceFilter(
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# label='SoftwareProduct (ID)',
|
||||
# )
|
||||
# software_product = django_filters.ModelMultipleChoiceFilter(
|
||||
# field_name='software_product__name',
|
||||
# queryset=SoftwareProduct.objects.all(),
|
||||
# to_field_name='name',
|
||||
# label='SoftwareProduct (name)',
|
||||
# )
|
||||
# tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product") # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilter(BaseFilter):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple() # "tag")
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(software_product__name__icontains=value) | Q(version__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -1,192 +0,0 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from netbox.forms import (
|
||||
NetBoxModelForm,
|
||||
NetBoxModelCSVForm,
|
||||
NetBoxModelBulkEditForm,
|
||||
NetBoxModelFilterSetForm,
|
||||
)
|
||||
from dcim.models import Manufacturer, Device
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description", "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
"""Form for filtering SoftwareProduct instances."""
|
||||
|
||||
model = SoftwareProduct
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product", "tags")
|
||||
|
||||
# def clean(self):
|
||||
# import pdb;pdb.set_trace()
|
||||
# return super(SoftwareProductVersionForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
"""Form for filtering SoftwareProductVersion instances."""
|
||||
|
||||
model = SoftwareProductVersion
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label="Name",
|
||||
)
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductVersionCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
virtualmachine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
# initial_params={
|
||||
# 'device_types': 'device_type'
|
||||
# }
|
||||
)
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
|
||||
),
|
||||
query_params={
|
||||
'software_product': '$software_product',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = ("device", "virtualmachine", "software_product", "version", "tags")
|
||||
|
||||
def clean(self):
|
||||
if not any([self.cleaned_data['device'], self.cleaned_data['virtualmachine']]):
|
||||
raise forms.ValidationError(_("Installation requires atleast one virtualmachine or device destination."))
|
||||
return super(SoftwareProductInstallationForm, self).clean()
|
||||
|
||||
|
||||
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
||||
"""Form for filtering SoftwareProductInstallation instances."""
|
||||
|
||||
model = SoftwareProductInstallation
|
||||
|
||||
q = forms.CharField(required=False, label="Search")
|
||||
|
||||
# tag = TagFilterField(SoftwareProduct)
|
||||
|
||||
|
||||
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False
|
||||
)
|
||||
model = SoftwareProductInstallation
|
||||
fieldsets = (
|
||||
(None, ('software_product', 'version',)),
|
||||
)
|
||||
# nullable_fields = ('',)
|
||||
@@ -1,98 +0,0 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import safestring
|
||||
# from extras.utils import extras_features # todo this changed in netbox 3.1.X and higher, currently not used but checkout why
|
||||
from netbox.models import NetBoxModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProduct(NetBoxModel):
|
||||
"""
|
||||
A SoftwareProduct represents ...
|
||||
"""
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductVersion(NetBoxModel):
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
||||
|
||||
def get_installation_count(self):
|
||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
||||
count=count
|
||||
)) if count else "0"
|
||||
|
||||
|
||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class SoftwareProductInstallation(NetBoxModel):
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
virtualmachine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.PROTECT,
|
||||
null = True,
|
||||
blank = True
|
||||
)
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
version = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProductVersion',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
def get_platform(self):
|
||||
return self.device or self.virtualmachine
|
||||
|
||||
def render_type(self):
|
||||
return f"{'device' if self.device else 'virtualmachine'}"
|
||||
@@ -1,65 +0,0 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproduct_list',
|
||||
link_text='Software Products',
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_add",
|
||||
"Add",
|
||||
"mdi mdi-plus-thick",
|
||||
ButtonColorChoices.GREEN,
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproduct_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproduct"],
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductversion_list',
|
||||
link_text='Versions',
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_add",
|
||||
"Add",
|
||||
"mdi mdi-plus-thick",
|
||||
ButtonColorChoices.GREEN,
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductversion_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductversion"],
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproductinstallation_list',
|
||||
link_text='Installations',
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_add",
|
||||
"Add",
|
||||
"mdi mdi-plus-thick",
|
||||
ButtonColorChoices.GREEN,
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwareproductinstallation_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwareproductinstallation"],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -1,167 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from django.db.models import Count
|
||||
from django_tables2.utils import Accessor
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from netbox.tables import NetBoxTable, ChoiceFieldColumn, ToggleColumn, columns
|
||||
|
||||
|
||||
class SoftwareProductTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProduct objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name="plugins:netbox_slm:softwareproduct_list",
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProduct
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"name",
|
||||
"description",
|
||||
"installations",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(
|
||||
count=Count('softwareproductinstallation__id')
|
||||
).order_by(("-" if is_descending else "") + "count")
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductVersionTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductVersion objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Version')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('software_product__manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name="plugins:netbox_slm:softwareproductversion_list",
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductVersion
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"software_product",
|
||||
"manufacturer",
|
||||
"installations",
|
||||
"tags",
|
||||
)
|
||||
sequence = (
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"name",
|
||||
"installations",
|
||||
)
|
||||
|
||||
def order_installations(self, queryset, is_descending):
|
||||
queryset = queryset.annotate(
|
||||
count=Count('softwareproductinstallation__id')
|
||||
).order_by(("-" if is_descending else "") + "count")
|
||||
return queryset, True
|
||||
|
||||
|
||||
class SoftwareProductInstallationTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareProductInstallation objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
device = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True
|
||||
)
|
||||
virtualmachine = tables.Column(
|
||||
accessor=Accessor('virtualmachine'),
|
||||
linkify=True
|
||||
)
|
||||
platform = tables.Column(
|
||||
accessor='get_platform',
|
||||
linkify=True
|
||||
)
|
||||
type = tables.Column(accessor='render_type')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
version = tables.Column(
|
||||
accessor=Accessor('version'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name="plugins:netbox_slm:softwareproductinstallation_list",
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareProductInstallation
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"platform",
|
||||
"type",
|
||||
"software_product",
|
||||
"version",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def order_platform(self, queryset, is_descending):
|
||||
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
|
||||
return queryset, True
|
||||
|
||||
def order_type(self, queryset, is_descending):
|
||||
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
|
||||
return queryset, True
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
<td>
|
||||
{% for version in object.softwareproduct_versions.all %}
|
||||
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
|
||||
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
|
||||
</a>
|
||||
{% empty %}
|
||||
n/a
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Installation
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.device %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ object.device }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">Virtualmachine</th>
|
||||
<td>{{ object.virtualmachine }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software Product Version
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installations</th>
|
||||
<td>
|
||||
{{ object.get_installation_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,67 +0,0 @@
|
||||
from django.urls import path
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
|
||||
urlpatterns = [
|
||||
# Software Products
|
||||
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
||||
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(),
|
||||
name="softwareproduct_bulk_delete"),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
||||
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(),
|
||||
name="softwareproduct_delete"),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
||||
path(
|
||||
"software-products/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproduct_changelog",
|
||||
kwargs={"model": SoftwareProduct},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
|
||||
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(),
|
||||
name="softwareproductversion_import"),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
||||
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(),
|
||||
name="softwareproductversion_bulk_delete"),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
||||
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(),
|
||||
name="softwareproductversion_delete"),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
||||
path(
|
||||
"versions/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductversion_changelog",
|
||||
kwargs={"model": SoftwareProductVersion},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("installations/", views.SoftwareProductInstallationListView.as_view(),
|
||||
name='softwareproductinstallation_list'),
|
||||
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(),
|
||||
name='softwareproductinstallation_add'),
|
||||
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(),
|
||||
name="softwareproductinstallation_import"),
|
||||
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(),
|
||||
name="softwareproductinstallation_bulk_edit"),
|
||||
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(),
|
||||
name="softwareproductinstallation_bulk_delete"),
|
||||
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(),
|
||||
name="softwareproductinstallation"),
|
||||
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(),
|
||||
name="softwareproductinstallation_delete"),
|
||||
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(),
|
||||
name="softwareproductinstallation_edit"),
|
||||
path(
|
||||
"installations/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwareproductinstallation_changelog",
|
||||
kwargs={"model": SoftwareProductInstallation},
|
||||
),
|
||||
]
|
||||
@@ -1,165 +0,0 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter
|
||||
from netbox_slm.forms import (
|
||||
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm,
|
||||
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
|
||||
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
|
||||
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
)
|
||||
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
filterset_form = SoftwareProductFilterForm
|
||||
table = SoftwareProductTable
|
||||
# template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproductversion_set.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = SoftwareProductCSVForm
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = SoftwareProductFilter
|
||||
table = SoftwareProductTable
|
||||
form = SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
filterset_form = SoftwareProductVersionFilterForm
|
||||
table = SoftwareProductVersionTable
|
||||
# template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = SoftwareProductVersionCSVForm
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = SoftwareProductVersionFilter
|
||||
table = SoftwareProductVersionTable
|
||||
form = SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
filterset_form = SoftwareProductInstallationFilterForm
|
||||
table = SoftwareProductInstallationTable
|
||||
# template_name = "netbox_slm/object_list.html"
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
# def get_extra_context(self, request, instance):
|
||||
# records = instance.record_set.all()
|
||||
# return {"records": records}
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = SoftwareProductInstallationCSVForm
|
||||
table = SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = SoftwareProductInstallationFilter
|
||||
table = SoftwareProductInstallationTable
|
||||
form = SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = SoftwareProductInstallationTable
|
||||
@@ -1,28 +0,0 @@
|
||||
[metadata]
|
||||
name = netbox-slm
|
||||
version = 0.97
|
||||
description = Software Lifecycle Management Netbox Plugin.
|
||||
# long_description = file: README.rst
|
||||
# long_description_content_type='text/x-rst'
|
||||
author = Hedde van der Heide
|
||||
author_email = hedde.vanderheide@ictu.nl
|
||||
license = Apache 2.0
|
||||
classifiers =
|
||||
Environment :: Web Environment
|
||||
Framework :: Django
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Topic :: Internet :: WWW/HTTP
|
||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
|
||||
[options]
|
||||
include_package_data = true
|
||||
packages = find:
|
||||
python_requires = >=3.7
|
||||
@@ -1,3 +0,0 @@
|
||||
from setuptools import setup, Extension
|
||||
|
||||
setup()
|
||||
@@ -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.4'
|
||||
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