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 | ||
|
|
9e81b94d31 | ||
|
|
18fed2c029 | ||
|
|
a02acbc561 | ||
|
|
74d648b94d | ||
|
|
e9422d70e3 |
@@ -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
|
**/netbox-docker
|
||||||
**/*.pyc
|
**/*.pyc
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
@@ -5,3 +6,8 @@
|
|||||||
**/.vscode
|
**/.vscode
|
||||||
**/dist
|
**/dist
|
||||||
**/netbox_slm.egg-info
|
**/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");
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
1. Definitions.
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
limitations under the License.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
@@ -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.
|
||||||
-214
@@ -1,214 +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
|
|
||||||
$ git checkout f5356b8 (to pin 3.0.9)
|
|
||||||
|
|
||||||
install the virtual environment
|
|
||||||
|
|
||||||
$ pipenv shell
|
|
||||||
$ pipenv install
|
|
||||||
|
|
||||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
|
||||||
|
|
||||||
DEBUG = True
|
|
||||||
SECRET_KEY = 'dummy'
|
|
||||||
DEVELOPER = True
|
|
||||||
PLUGINS = [
|
|
||||||
'netbox_slm',
|
|
||||||
]
|
|
||||||
|
|
||||||
The Netbox installation above will be used to run Django management
|
|
||||||
commands like runserver, makemigrations and migrate, which will be
|
|
||||||
explained in the next steps below;
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
from your projects directory clone the netbox-slm repository
|
|
||||||
|
|
||||||
$ git clone https://github.com/ICTU/netbox_slm
|
|
||||||
$ cd netbox_slm
|
|
||||||
$ ./start-netbox.sh
|
|
||||||
|
|
||||||
This will start Netbox locally (requires Docker) and forward the redis
|
|
||||||
and postgres ports to the localhost (make sure there’s no processes
|
|
||||||
using these ports or change the dockerfiles accordingly)
|
|
||||||
|
|
||||||
Note, you can also start and stop netbox by hand:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ cd netbox-docker
|
|
||||||
$ docker-compose up -d
|
|
||||||
|
|
||||||
or stop the stack with
|
|
||||||
|
|
||||||
$ docker-compose down
|
|
||||||
|
|
||||||
# to start fresh:
|
|
||||||
|
|
||||||
$ docker-compose down
|
|
||||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
|
||||||
$ docker-compose up -d --force-recreate
|
|
||||||
|
|
||||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
|
||||||
|
|
||||||
Go back to the netbox configuration.py file and update the postgres and
|
|
||||||
redis connection strings (username, password) to the ones the netbox
|
|
||||||
docker backend is using, for example (using default user and passwords
|
|
||||||
from the netbox docker example):
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
<<collapsed>>
|
|
||||||
|
|
||||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
|
||||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
|
||||||
DATABASE = {
|
|
||||||
'NAME': 'netbox', # Database name
|
|
||||||
'USER': 'netbox', # PostgreSQL username
|
|
||||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
|
||||||
'HOST': 'localhost', # Database server
|
|
||||||
'PORT': '', # Database port (leave blank for default)
|
|
||||||
'CONN_MAX_AGE': 300, # Max database connection age
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
|
||||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
|
||||||
# to use two separate database IDs.
|
|
||||||
REDIS = {
|
|
||||||
'tasks': {
|
|
||||||
'HOST': 'localhost',
|
|
||||||
'PORT': 6379,
|
|
||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
|
||||||
'PASSWORD': 'H733Kdjndks81',
|
|
||||||
'DATABASE': 0,
|
|
||||||
'SSL': False,
|
|
||||||
# Set this to True to skip TLS certificate verification
|
|
||||||
# This can expose the connection to attacks, be careful
|
|
||||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
|
||||||
},
|
|
||||||
'caching': {
|
|
||||||
'HOST': 'localhost',
|
|
||||||
'PORT': 6379,
|
|
||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
|
||||||
'PASSWORD': 'H733Kdjndks81',
|
|
||||||
'DATABASE': 1,
|
|
||||||
'SSL': False,
|
|
||||||
# Set this to True to skip TLS certificate verification
|
|
||||||
# This can expose the connection to attacks, be careful
|
|
||||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<<collapsed>>
|
|
||||||
|
|
||||||
Now you can run commands from the netbox repository like this;
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ cd netbox/netbox
|
|
||||||
$ export PYTHONPATH=../../netbox-slm/src/
|
|
||||||
$ python3 manage.py migrate netbox_slm
|
|
||||||
$ python3 manage.py runserver 8001
|
|
||||||
|
|
||||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
|
||||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
|
||||||
@@ -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,
|
SoftwareProductViewSet,
|
||||||
SoftwareProductVersionViewSet,
|
SoftwareProductVersionViewSet,
|
||||||
SoftwareProductInstallationViewSet,
|
SoftwareProductInstallationViewSet,
|
||||||
|
SoftwareLicenseViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = NetBoxRouter()
|
router = NetBoxRouter()
|
||||||
@@ -12,4 +13,6 @@ router.APIRootView = NetboxSLMRootView
|
|||||||
router.register("softwareproducts", SoftwareProductViewSet)
|
router.register("softwareproducts", SoftwareProductViewSet)
|
||||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||||
|
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||||
|
|
||||||
urlpatterns = router.urls
|
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 *
|
|
||||||
-223
@@ -1,223 +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 generic `Netbox plugin
|
|
||||||
instructions <https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins>`__)
|
|
||||||
to install the Netbox SLM plugin:
|
|
||||||
|
|
||||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
|
||||||
``configuration/configuration.py``.
|
|
||||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.93`` as
|
|
||||||
contents.
|
|
||||||
3. Create a ``Dockerfile-SLM`` with contents:
|
|
||||||
|
|
||||||
.. code:: dockerfile
|
|
||||||
|
|
||||||
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
|
|
||||||
|
|
||||||
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 and setup.cfg
|
|
||||||
$ 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.
|
|
||||||
|
|
||||||
Gotcha’s
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
*Netbox’s exception handler seems to supress initial Exceptions,
|
|
||||||
obfuscating missing imports or other Django related issues by re-raising
|
|
||||||
an opague error, e.g. netbox_slm plugin namespace doesnotexist. It’s
|
|
||||||
recommended to often check the runtime server or patch Netbox’s global
|
|
||||||
exception handler.*
|
|
||||||
|
|
||||||
Steps
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
from your projects directory clone the netbox repository
|
|
||||||
|
|
||||||
$ git clone https://github.com/netbox-community/netbox
|
|
||||||
$ cd netbox
|
|
||||||
$ git checkout f5356b8 (to pin 3.0.9)
|
|
||||||
|
|
||||||
install the virtual environment
|
|
||||||
|
|
||||||
$ pipenv shell
|
|
||||||
$ pipenv install
|
|
||||||
|
|
||||||
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
|
|
||||||
|
|
||||||
DEBUG = True
|
|
||||||
SECRET_KEY = 'dummy'
|
|
||||||
DEVELOPER = True
|
|
||||||
PLUGINS = [
|
|
||||||
'netbox_slm',
|
|
||||||
]
|
|
||||||
|
|
||||||
The Netbox installation above will be used to run Django management
|
|
||||||
commands like runserver, makemigrations and migrate, which will be
|
|
||||||
explained in the next steps below;
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
from your projects directory clone the netbox-slm repository
|
|
||||||
|
|
||||||
$ git clone https://github.com/ICTU/netbox_slm
|
|
||||||
$ cd netbox_slm
|
|
||||||
$ ./start-netbox.sh
|
|
||||||
|
|
||||||
This will start Netbox locally (requires Docker) and forward the redis
|
|
||||||
and postgres ports to the localhost (make sure there’s no processes
|
|
||||||
using these ports or change the dockerfiles accordingly)
|
|
||||||
|
|
||||||
Note, you can also start and stop netbox by hand:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ cd netbox-docker
|
|
||||||
$ docker-compose up -d
|
|
||||||
|
|
||||||
or stop the stack with
|
|
||||||
|
|
||||||
$ docker-compose down
|
|
||||||
|
|
||||||
# to start fresh:
|
|
||||||
|
|
||||||
$ docker-compose down
|
|
||||||
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
|
|
||||||
$ docker-compose up -d --force-recreate
|
|
||||||
|
|
||||||
this will require you to re-run the migrate commando's for netbox-slm, see further down below
|
|
||||||
|
|
||||||
Go back to the netbox configuration.py file and update the postgres and
|
|
||||||
redis connection strings (username, password) to the ones the netbox
|
|
||||||
docker backend is using, for example (using default user and passwords
|
|
||||||
from the netbox docker example):
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
<<collapsed>>
|
|
||||||
|
|
||||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
|
||||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
|
||||||
DATABASE = {
|
|
||||||
'NAME': 'netbox', # Database name
|
|
||||||
'USER': 'netbox', # PostgreSQL username
|
|
||||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
|
||||||
'HOST': 'localhost', # Database server
|
|
||||||
'PORT': '', # Database port (leave blank for default)
|
|
||||||
'CONN_MAX_AGE': 300, # Max database connection age
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
|
||||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
|
||||||
# to use two separate database IDs.
|
|
||||||
REDIS = {
|
|
||||||
'tasks': {
|
|
||||||
'HOST': 'localhost',
|
|
||||||
'PORT': 6379,
|
|
||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
|
||||||
'PASSWORD': 'H733Kdjndks81',
|
|
||||||
'DATABASE': 0,
|
|
||||||
'SSL': False,
|
|
||||||
# Set this to True to skip TLS certificate verification
|
|
||||||
# This can expose the connection to attacks, be careful
|
|
||||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
|
||||||
},
|
|
||||||
'caching': {
|
|
||||||
'HOST': 'localhost',
|
|
||||||
'PORT': 6379,
|
|
||||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
|
||||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
|
||||||
# 'SENTINEL_SERVICE': 'netbox',
|
|
||||||
'PASSWORD': 'H733Kdjndks81',
|
|
||||||
'DATABASE': 1,
|
|
||||||
'SSL': False,
|
|
||||||
# Set this to True to skip TLS certificate verification
|
|
||||||
# This can expose the connection to attacks, be careful
|
|
||||||
# 'INSECURE_SKIP_TLS_VERIFY': False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<<collapsed>>
|
|
||||||
|
|
||||||
Now you can run commands from the netbox repository like this;
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ cd netbox/netbox
|
|
||||||
$ export PYTHONPATH=../../netbox-slm/src/
|
|
||||||
$ python3 manage.py migrate netbox_slm
|
|
||||||
$ python3 manage.py runserver 8001
|
|
||||||
|
|
||||||
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
|
|
||||||
version of the netbox UI. Port 8000 is taken by the docker ran variant.
|
|
||||||
@@ -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.95'
|
|
||||||
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,104 +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,
|
|
||||||
related_name='software_products',
|
|
||||||
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,
|
|
||||||
related_name='softwareproduct_versions'
|
|
||||||
)
|
|
||||||
name = models.CharField(max_length=64)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
def get_installation_count(self):
|
|
||||||
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
|
|
||||||
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
|
|
||||||
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
|
|
||||||
count=count
|
|
||||||
)) if count else "0"
|
|
||||||
|
|
||||||
|
|
||||||
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class SoftwareProductInstallation(NetBoxModel):
|
|
||||||
device = models.ForeignKey(
|
|
||||||
to='dcim.Device',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='softwareproduct_installations',
|
|
||||||
null=True,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
virtualmachine = models.ForeignKey(
|
|
||||||
to='virtualization.VirtualMachine',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='softwareproduct_installations',
|
|
||||||
null = True,
|
|
||||||
blank = True
|
|
||||||
)
|
|
||||||
software_product = models.ForeignKey(
|
|
||||||
to='netbox_slm.SoftwareProduct',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='software_products'
|
|
||||||
)
|
|
||||||
version = models.ForeignKey(
|
|
||||||
to='netbox_slm.SoftwareProductVersion',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='softwareproduct_versions'
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
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,146 +0,0 @@
|
|||||||
import django_tables2 as tables
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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 render_software_product(self, value, **kwargs):
|
|
||||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
|
||||||
@@ -1,44 +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>
|
|
||||||
{% 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.softwareproduct_versions.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.95
|
|
||||||
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