70 Commits
Author SHA1 Message Date
MrBlake e1c817229b feat: aufräumaktion 2026-07-24 15:22:59 +02:00
MrBlake 01d40ddecc feat: add tenancy support for software licenses
Add tenant group and tenant assignments across the model, forms, filters, API, tables, imports, and tests. Replace the Docker-based setup with Git/Pip installation documentation for NetBox 4.6.5.
2026-07-24 15:22:08 +02:00
wkoot 8a2dabc382 Update README.md 2026-06-25 18:18:33 +02:00
wkootandArtem Kotik f84a6bde2a Version bump to NetBox v4.6.3 (#82)
* Add support for NetBox 4.6
* Test NetBox v4.6.3 and Python 3.14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #21 and #23

* Changed Fieldtypes of Start and Expiration Date

Closes #22

* Bump to NetBox v3.6.1

* Add pyproject.toml and python workflow

---------

Co-authored-by: Manuel Hartung <manuel.hartung@atos.net>
2023-09-15 09:41:36 +02:00
wkoot 6c512e0e9b Add SoftwareLicense (#20) 2023-06-30 15:11:28 +02:00
wkoot 5b96496b46 Replace NetBoxModelCSVForm with NetBoxModelImportForm (#19)
Closes #18
2023-06-06 21:16:13 +02:00
wkoot 88edcde8d6 Unify api serializers (#16) 2023-04-19 10:14:32 +02:00
wkoot 7f18a3ad6f Version bump NetBox 3.4.7 (#15)
Closes #14
2023-03-31 10:02:35 +02:00
wkoot 1925be271a Set up continuous integration (#13)
Closes #3
2023-03-29 22:00:48 +02:00
Hedde van der Heide a53486ca23 bump version to 1.2 2022-05-19 11:08:57 +02:00
Hedde van der Heide 4bc418a5d0 fixed filter bug versions api and set required on manufacturer for softwareproducts add 2022-05-19 11:06:34 +02:00
Hedde van der Heide e952111a53 version bump 1.1 2022-05-03 15:21:18 +02:00
Hedde van der Heide e73cb5fa1a Merge branch 'master' of https://github.com/ICTU/netbox_slm 2022-05-03 15:19:55 +02:00
Hedde van der Heide 56552bdd02 fixed filter search 2022-05-03 15:19:53 +02:00
Hedde van der Heide f914e78812 Update README.md 2022-05-02 12:39:47 +02:00
Hedde van der Heide c9a7b2315e Update README.md 2022-05-02 12:37:36 +02:00
Hedde van der Heide a8c9fc5fbe Delete LICENSE.md 2022-05-02 12:37:18 +02:00
Hedde van der Heide a1f6e3dc92 Create LICENSE 2022-05-02 12:37:11 +02:00
Hedde van der Heide 4f8053c0f6 Rename LICENSE to LICENSE.md 2022-05-02 12:32:20 +02:00
Hedde van der Heide 3b3a6d63d4 Update and rename README.rst to README.md 2022-05-02 12:32:08 +02:00
Hedde van der Heide ce283dea52 updated README 2022-04-22 14:04:29 +02:00
Hedde van der Heide f3579cce63 updated README 2022-04-22 13:57:49 +02:00
Hedde van der Heide 3bb711ea84 updated README 2022-04-22 13:57:45 +02:00
Hedde van der Heide 9d76bb421d normalized project structure 2022-04-22 13:52:26 +02:00
Hedde van der Heide 7a24221444 moved top level files 2022-04-22 13:33:17 +02:00
Hedde van der Heide ac198ef7cf Large overhaul of naming conventions and code clean ups 2022-04-22 13:30:10 +02:00
Hedde van der Heide 6d05b1b6ca bumped version 2022-04-15 09:09:41 +02:00
Hedde van der Heide 049fb045c0 added missing migrations (though stale) 2022-04-15 09:06:28 +02:00
Hedde van der Heide 9e81b94d31 bumped version 2022-04-15 09:02:19 +02:00
Hedde van der Heide 18fed2c029 fixed reverse relationship naming (to default) and added ordering methods for calculated attributes 2022-04-15 09:00:36 +02:00
Hedde van der Heide a02acbc561 fixed installation ordering issue with field methods 2022-04-14 11:35:25 +02:00
Hedde van der Heide 74d648b94d update README.rst, added warning 2022-04-10 11:24:50 +02:00
Hedde van der Heide e9422d70e3 update README.rst 2022-04-10 11:22:33 +02:00
Hedde van der Heide 15b61fd7f9 bumped version for next release 2022-04-10 11:18:22 +02:00
Hedde van der Heide c264536850 Merge branch 'master' of https://github.com/ICTU/netbox_slm 2022-04-10 11:10:06 +02:00
Hedde van der Heide 2dbcaa4877 3.2.X compatibility fixes 2022-04-10 11:10:00 +02:00
Hedde van der Heide 6884bb3b94 Merge remote-tracking branch 'origin/feature/netbox-3.1.X-compatibility' 2022-04-07 09:31:42 +02:00
Hedde van der Heide bf47320864 fixed import no longer working under netbox >=3.1.X: 2022-04-07 09:30:28 +02:00
Hedde van der Heide f7bb83c913 Update README.rst 2022-03-28 12:35:56 +02:00
Hedde van der Heide 0519796af6 Update README.rst 2022-03-28 12:35:32 +02:00
Hedde van der Heide 19fc473286 version bump 0.94 2022-03-28 11:14:03 +02:00
Hedde van der Heide f4188b957f fixed 404 bug, added default ordering for some tables 2022-03-28 11:07:33 +02:00
Hedde van der Heide 51736942e7 fixed display bug installations detail, merged device/vm columns and added type for installations table view 2022-03-24 11:15:53 +01:00
Hedde van der Heide adea4437c9 fixed breaking changes (partially, templates require another fix) 2021-12-23 11:09:58 +01:00
84 changed files with 3217 additions and 2345 deletions
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
COMPOSE_PROJECT_NAME=netbox-docker
+6
View File
@@ -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/
+132
View File
@@ -0,0 +1,132 @@
# Changelog
## [Unreleased]
### Added
* Optional tenant group and tenant assignments for software licenses
### Changed
* Support NetBox v4.6.5
* Replace the Docker-based setup with Git/Pip installation documentation
## [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
+198 -10
View File
@@ -1,13 +1,201 @@
Copyright 2022 ICTU Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Licensed under the Apache License, Version 2.0 (the "License"); 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.
+7
View File
@@ -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__
-12
View File
@@ -1,12 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
twine = "3.7.1"
[requires]
python_version = "3.9"
Generated
-192
View File
@@ -1,192 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "f3cebe84c169ba6773cfcfae65b47e5076fa331e445f47a6ec4d13306942deb4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {},
"develop": {
"bleach": {
"hashes": [
"sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
"sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
],
"markers": "python_version >= '3.6'",
"version": "==4.1.0"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
],
"markers": "python_version >= '3'",
"version": "==2.0.10"
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.4"
},
"docutils": {
"hashes": [
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.18.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"importlib-metadata": {
"hashes": [
"sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
"sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
],
"markers": "python_version >= '3.7'",
"version": "==4.10.0"
},
"keyring": {
"hashes": [
"sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
"sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"
],
"markers": "python_version >= '3.7'",
"version": "==23.5.0"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pkginfo": {
"hashes": [
"sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
"sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
],
"version": "==1.8.2"
},
"pygments": {
"hashes": [
"sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4",
"sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"
],
"markers": "python_version >= '3.5'",
"version": "==2.11.1"
},
"pyparsing": {
"hashes": [
"sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
"sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.6"
},
"readme-renderer": {
"hashes": [
"sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
"sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
],
"markers": "python_version >= '3.6'",
"version": "==32.0"
},
"requests": {
"hashes": [
"sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5",
"sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.27.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"rfc3986": {
"hashes": [
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.5.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.16.0"
},
"tqdm": {
"hashes": [
"sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c",
"sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.62.3"
},
"twine": {
"hashes": [
"sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
"sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
],
"index": "pypi",
"version": "==3.7.1"
},
"urllib3": {
"hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.7"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"zipp": {
"hashes": [
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
],
"markers": "python_version >= '3.7'",
"version": "==3.7.0"
}
}
}
+116
View File
@@ -0,0 +1,116 @@
# 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.
## 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
- Lizenzen Mandantengruppen und Mandanten zuordnen
- Bedienung über die NetBox-Oberfläche und REST-API
- Filter, Bulk-Import und Bulk-Bearbeitung
## 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 "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
```
### 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.
-223
View File
@@ -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
$ 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.
Gotchas
~~~~~~~~
*Netboxs 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. Its
recommended to often check the runtime server or patch Netboxs 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 theres no processes
using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand:
::
$ cd netbox-docker
$ docker-compose up -d
or stop the stack with
$ docker-compose down
# to start fresh:
$ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate
this will require you to re-run the migrate commando's for netbox-slm, see further down below
Go back to the netbox configuration.py file and update the postgres and
redis connection strings (username, password) to the ones the netbox
docker backend is using, for example (using default user and passwords
from the netbox docker example):
::
<<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
<<collapsed>>
Now you can run commands from the netbox repository like this;
::
$ cd netbox/netbox
$ export PYTHONPATH=../../netbox-slm/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.
+39
View File
@@ -0,0 +1,39 @@
"""
Copyright 2022-2025 ICTU
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from netbox.plugins import PluginConfig
__version__ = "1.9.0"
class SLMConfig(PluginConfig):
name = "netbox_slm"
verbose_name = "Software Lifecycle Management"
version = __version__
description = "Software Lifecycle Management Netbox Plugin."
author = "ICTU"
author_email = "open-source-projects@ictu.nl"
base_url = "slm"
required_settings = []
default_settings = {
"top_level_menu": True,
"link_cluster_installations": "right",
"link_device_installations": "right",
"link_virtualmachine_installations": "right",
}
config = SLMConfig
+7
View File
@@ -0,0 +1,7 @@
from django.apps import apps
from django.conf import settings
from django.contrib import admin
if settings.DEBUG:
for model in apps.get_app_config("netbox_slm").get_models():
admin.site.register(model)
+136
View File
@@ -0,0 +1,136 @@
from rest_framework import serializers
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
from netbox.api.serializers import NetBoxModelSerializer
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
class SoftwareLicenseSerializer(NetBoxModelSerializer):
display = SerializerMethodField()
url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
class Meta:
model = SoftwareLicense
fields = (
"id",
"display",
"url",
"name",
"description",
"type",
"spdx_expression",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"support",
"license_amount",
"software_product",
"version",
"installation",
"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):
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."}
)
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}"
+18
View File
@@ -0,0 +1,18 @@
from netbox.api.routers import NetBoxRouter
from netbox_slm.api.views import (
NetboxSLMRootView,
SoftwareProductViewSet,
SoftwareProductVersionViewSet,
SoftwareProductInstallationViewSet,
SoftwareLicenseViewSet,
)
router = NetBoxRouter()
router.APIRootView = NetboxSLMRootView
router.register("softwareproducts", SoftwareProductViewSet)
router.register("softwareproductversions", SoftwareProductVersionViewSet)
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
router.register("softwarelicenses", SoftwareLicenseViewSet)
urlpatterns = router.urls
+50
View File
@@ -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
+149
View File
@@ -0,0 +1,149 @@
from django.db.models import Q
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
from dcim.models import Device, Manufacturer
from netbox.filtersets import NetBoxModelFilterSet
from netbox_slm.models import (
SoftwareProduct,
SoftwareProductVersion,
SoftwareProductInstallation,
SoftwareLicense,
SoftwareReleaseTypes,
)
from virtualization.models import Cluster, VirtualMachine
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")
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",)
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)
+24
View File
@@ -0,0 +1,24 @@
from .software_license import (
SoftwareLicenseForm,
SoftwareLicenseFilterForm,
SoftwareLicenseBulkImportForm,
SoftwareLicenseBulkEditForm,
)
from .software_product import (
SoftwareProductForm,
SoftwareProductFilterForm,
SoftwareProductBulkImportForm,
SoftwareProductBulkEditForm,
)
from .software_product_installation import (
SoftwareProductInstallationForm,
SoftwareProductInstallationFilterForm,
SoftwareProductInstallationBulkImportForm,
SoftwareProductInstallationBulkEditForm,
)
from .software_product_version import (
SoftwareProductVersionForm,
SoftwareProductVersionFilterForm,
SoftwareProductVersionBulkImportForm,
SoftwareProductVersionBulkEditForm,
)
+243
View File
@@ -0,0 +1,243 @@
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField
from django.urls import reverse_lazy
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import (
SoftwareProduct,
SoftwareProductVersion,
SoftwareProductInstallation,
SoftwareLicense,
spdx_license_names,
)
from 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()
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression")
stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker())
expiration_date = DateField(required=False, widget=DatePicker())
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params=dict(software_product="$software_product"),
)
installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
required=False,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
),
query_params=dict(software_product="$software_product"),
)
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",
"support",
"license_amount",
"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",
"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)
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",
"support",
"license_amount",
"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",
"support",
"license_amount",
"software_product",
"version",
"installation",
"tenant_group",
"tenant",
),
)
nullable_fields = (
"type",
"spdx_expression",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"support",
"license_amount",
"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())
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
license_amount = IntegerField(required=False, min_value=0)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params=dict(software_product="$software_product"),
)
installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
required=False,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
),
query_params=dict(software_product="$software_product"),
)
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",
)
+75
View File
@@ -0,0 +1,75 @@
from django.forms import CharField
from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import SoftwareProduct
from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
class SoftwareProductForm(NetBoxModelForm):
comments = CommentField()
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
)
class Meta:
model = SoftwareProduct
fields = (
"name",
"description",
"manufacturer",
"tags",
"comments",
)
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct
fieldsets = (
FieldSet("q", "filter_id", "tag"),
FieldSet("name", "description", "manufacturer_id"),
)
selector_fields = ("q", "filter_id", "name")
tag = TagFilterField(model)
name = CharField(required=False)
description = CharField(required=False)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label="Manufacturer",
)
class SoftwareProductBulkImportForm(NetBoxModelImportForm):
class Meta:
model = SoftwareProduct
fields = (
"name",
"description",
"manufacturer",
"tags",
)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
model = SoftwareProduct
fieldsets = (FieldSet("manufacturer"),)
nullable_fields = ("manufacturer", "comments")
tag = TagFilterField(model)
comments = CommentField()
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
)
@@ -0,0 +1,138 @@
from django.forms import ValidationError
from django.urls import reverse_lazy
from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect
from virtualization.models import Cluster, VirtualMachine
class SoftwareProductInstallationForm(NetBoxModelForm):
comments = CommentField()
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params=dict(software_product="$software_product"),
)
class Meta:
model = SoftwareProductInstallation
fields = (
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
"comments",
)
def clean_version(self):
version = self.cleaned_data["version"]
software_product = self.cleaned_data["software_product"]
if version not in software_product.softwareproductversion_set.all():
raise ValidationError(
f"Version '{version}' doesn't exist on {software_product}, make sure you've "
f"selected a compatible version or first select the software product."
)
return version
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductInstallation
fieldsets = (
FieldSet("q", "filter_id", "tag"),
FieldSet("device_id", "virtualmachine_id", "cluster_id", "software_product_id", "version_id"),
)
selector_fields = ("filter_id", "q")
tag = TagFilterField(model)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label="Device",
)
virtualmachine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label="Cluster",
)
software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
)
version_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
label="Version",
)
class SoftwareProductInstallationBulkImportForm(NetBoxModelImportForm):
class Meta:
model = SoftwareProductInstallation
fields = (
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
)
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
model = SoftwareProductInstallation
fieldsets = (FieldSet("device", "virtualmachine", "cluster", "software_product", "version"),)
nullable_fields = ("device", "virtualmachine", "cluster", "comments")
tag = TagFilterField(model)
comments = CommentField()
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params=dict(software_product="$software_product"),
)
@@ -0,0 +1,126 @@
from django.forms import DateField, CharField, MultipleChoiceField
from django.urls import reverse_lazy
from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes
from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect, DatePicker
class SoftwareProductVersionForm(NetBoxModelForm):
comments = CommentField()
release_date = DateField(required=False, widget=DatePicker())
end_of_support = DateField(required=False, widget=DatePicker())
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
)
class Meta:
model = SoftwareProductVersion
fields = (
"name",
"description",
"software_product",
"release_date",
"documentation_url",
"end_of_support",
"filename",
"file_checksum",
"file_link",
"release_type",
"tags",
"comments",
)
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductVersion
fieldsets = (
FieldSet("q", "filter_id", "tag"),
FieldSet("name", "description", "filename", "release_type", "manufacturer_id", "software_product_id"),
)
selector_fields = ("q", "filter_id", "name")
tag = TagFilterField(model)
name = CharField(required=False)
description = CharField(required=False)
filename = CharField(required=False)
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label="Manufacturer",
)
software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
)
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm):
release_type = CharField(help_text=f"Release type (possible values: {SoftwareReleaseTypes.values})")
class Meta:
model = SoftwareProductVersion
fields = (
"name",
"description",
"software_product",
"release_date",
"end_of_support",
"filename",
"file_checksum",
"release_type",
"tags",
)
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
model = SoftwareProductVersion
fieldsets = (
FieldSet(
"release_date",
"documentation_url",
"end_of_support",
"filename",
"file_checksum",
"file_link",
"release_type",
"software_product",
),
)
nullable_fields = ("release_date", "documentation_url", "end_of_support", "filename", "file_checksum", "file_link")
tag = TagFilterField(model)
comments = CommentField()
release_date = DateField(required=False, widget=DatePicker())
documentation_url = CharField(required=False)
end_of_support = DateField(required=False, widget=DatePicker())
filename = CharField(required=False)
file_checksum = CharField(required=False)
file_link = CharField(required=False)
release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
)
@@ -0,0 +1,43 @@
# Generated by Django 3.2.9 on 2022-04-07 12:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0002_alter_softwareproduct_manufacturer'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproduct',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='softwareproductversion',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproductversion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
]
@@ -0,0 +1,46 @@
# Generated by Django 3.2.9 on 2022-04-15 07:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0153_created_datetimefield'),
('virtualization', '0029_created_datetimefield'),
('netbox_slm', '0003_auto_20220407_1209'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='software_product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='version',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='virtualmachine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'),
),
migrations.AlterField(
model_name='softwareproductversion',
name='software_product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
),
]
@@ -0,0 +1,54 @@
# Generated by Django 4.1.8 on 2023-06-27 08:10
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('extras', '0084_staging'),
('netbox_slm', '0004_auto_20220415_0705'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.AlterField(
model_name='softwareproductversion',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.CreateModel(
name='SoftwareLicense',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('name', models.CharField(max_length=128)),
('description', models.CharField(blank=True, max_length=255, null=True)),
('type', models.CharField(max_length=128)),
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
('start_date', models.DateField(blank=True, null=True)),
('expiration_date', models.DateField(blank=True, null=True)),
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
],
options={
'abstract': False,
},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.2.5 on 2023-09-22 00:34
from django.db import migrations
import netbox_slm.models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0005_add_software_license'),
]
operations = [
migrations.AddField(
model_name='softwarelicense',
name='stored_location_url',
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.2.5 on 2023-10-04 10:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0036_virtualmachine_config_template'),
('netbox_slm', '0006_softwarelicense_stored_location_url'),
]
operations = [
migrations.AddField(
model_name='softwareproductinstallation',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
),
migrations.AlterField(
model_name='softwarelicense',
name='installation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
),
migrations.AddConstraint(
model_name='softwareproductinstallation',
constraint=models.CheckConstraint(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",
),
),
]
+198
View File
@@ -0,0 +1,198 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.html import format_html
from license_expression import Licensing, get_spdx_licensing
from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet
from utilities.validators import EnhancedURLValidator
spdx_licensing: Licensing = get_spdx_licensing()
class LaxURLField(models.URLField):
"""
NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField
Overriding default_validators is needed, as they are always added
"""
default_validators = [EnhancedURLValidator()]
class SoftwareProduct(NetBoxModel):
name = models.CharField(max_length=128)
comments = models.TextField(blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
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()
search_target = reverse(
"plugins:netbox_slm:softwareproductinstallation_list", query={"software_product_id": self.pk}
)
return format_html("<a href='{}'>{}</a>", search_target, count) if count else "0"
class SoftwareReleaseTypes(models.TextChoices):
ALPHA = "A", "Alpha"
BETA = "B", "Beta"
RELEASE_CANDIDATE = "RC", "Release candidate"
STABLE = "S", "Stable release"
class SoftwareProductVersion(NetBoxModel):
name = models.CharField(max_length=64)
comments = models.TextField(blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
release_date = models.DateField(null=True, blank=True)
documentation_url = LaxURLField(max_length=1024, null=True, blank=True)
end_of_support = models.DateField(null=True, blank=True)
filename = models.CharField(max_length=64, null=True, blank=True)
file_checksum = models.CharField(max_length=128, null=True, blank=True)
file_link = LaxURLField(max_length=1024, null=True, blank=True)
release_type = models.CharField(
max_length=3,
choices=SoftwareReleaseTypes.choices,
default=SoftwareReleaseTypes.STABLE,
)
software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct",
on_delete=models.PROTECT,
)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
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(blank=True)
device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
virtualmachine = models.ForeignKey(
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
)
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return f"{self.pk} ({self.platform})"
class Meta:
constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_platform",
condition=(
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
),
violation_error_message="Installation requires exactly one platform destination.",
)
]
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
@property
def platform(self):
return self.device or self.virtualmachine or self.cluster
def render_type(self):
if self.device:
return "device"
if self.virtualmachine:
return "virtualmachine"
return "cluster"
def spdx_license_names():
names = [(item[0], item[0]) for item in spdx_licensing.known_symbols.items() if not item[1].is_exception]
names.sort()
names.insert(0, ("", "---------")) # set default value
return names
def validate_spdx_expression(value):
expression_info = spdx_licensing.validate(value)
if expression_info.errors:
raise ValidationError(f"{value} is not a known SPDX license expression")
class SoftwareLicense(NetBoxModel):
name = models.CharField(max_length=128)
comments = models.TextField(blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
type = models.CharField(max_length=128)
spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression])
stored_location = models.CharField(max_length=255, null=True, blank=True)
stored_location_url = LaxURLField(max_length=1024, null=True, blank=True)
start_date = models.DateField(null=True, blank=True)
expiration_date = models.DateField(null=True, blank=True)
support = models.BooleanField(default=None, null=True, blank=True)
license_amount = models.PositiveIntegerField(default=None, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
installation = models.ForeignKey(
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
)
tenant_group = models.ForeignKey(
to="tenancy.TenantGroup",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="software_licenses",
)
tenant = models.ForeignKey(
to="tenancy.Tenant",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="software_licenses",
)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
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."})
@property
def stored_location_txt(self):
if self.stored_location_url and not self.stored_location:
return "Link"
return self.stored_location
+91
View File
@@ -0,0 +1,91 @@
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
from . import SLMConfig
slm_items = (
PluginMenuItem(
link="plugins:netbox_slm:softwareproduct_list",
link_text="Software Products",
permissions=["netbox_slm.add_softwareproduct"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add",
"Add",
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproduct"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_bulk_import",
"Import",
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproduct"],
),
),
),
PluginMenuItem(
link="plugins:netbox_slm:softwareproductversion_list",
link_text="Versions",
permissions=["netbox_slm.add_softwareproductversion"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add",
"Add",
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductversion"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_bulk_import",
"Import",
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductversion"],
),
),
),
PluginMenuItem(
link="plugins:netbox_slm:softwareproductinstallation_list",
link_text="Installations",
permissions=["netbox_slm.add_softwareproductinstallation"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add",
"Add",
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductinstallation"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
"Import",
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductinstallation"],
),
),
),
PluginMenuItem(
link="plugins:netbox_slm:softwarelicense_list",
link_text="Licenses",
permissions=["netbox_slm.add_softwarelicense"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add",
"Add",
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwarelicense"],
),
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_bulk_import",
"Import",
"mdi mdi-upload",
permissions=["netbox_slm.import_softwarelicense"],
),
),
),
)
if get_plugin_config("netbox_slm", "top_level_menu"):
menu = PluginMenu(
label="Software Lifecycle",
groups=((SLMConfig.verbose_name, slm_items),),
icon_class="mdi mdi-content-save",
)
else:
# auto imported by default PluginConfig.menu_items = navigation.menu_items
menu_items = slm_items
+187
View File
@@ -0,0 +1,187 @@
import django_tables2 as tables
from django.db.models import Count, F, Value
from netbox.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
class SoftwareProductTable(NetBoxTable):
"""Table for displaying SoftwareProduct objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count")
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProduct
fields = (
"pk",
"name",
"description",
"manufacturer",
"installations",
"tags",
)
default_columns = (
"pk",
"name",
"manufacturer",
"installations",
"tags",
)
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
class SoftwareProductVersionTable(NetBoxTable):
"""Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count")
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProductVersion
fields = (
"pk",
"name",
"description",
"software_product",
"manufacturer",
"release_date",
"end_of_support",
"release_type",
"installations",
"tags",
)
default_columns = (
"pk",
"name",
"manufacturer",
"software_product",
"release_date",
"installations",
"tags",
)
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
class SoftwareProductInstallationTable(NetBoxTable):
"""Table for displaying SoftwareProductInstallation objects."""
pk = ToggleColumn()
platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type")
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
version = tables.Column(accessor="version", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProductInstallation
fields = (
"pk",
"platform",
"type",
"manufacturer",
"software_product",
"version",
"tags",
)
default_columns = (
"pk",
"platform",
"type",
"manufacturer",
"software_product",
"version",
"tags",
)
def order_platform(self, queryset, is_descending):
device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name"))
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name"))
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True
def order_type(self, queryset, is_descending):
device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine"))
cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster"))
queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate)
return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True
class SoftwareLicenseTable(NetBoxTable):
"""Table for displaying SoftwareLicense objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
type = tables.Column()
spdx_expression = tables.Column(verbose_name="SPDX expression")
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
version = tables.Column(accessor="version", linkify=True)
installation = tables.Column(accessor="installation", linkify=True)
tenant_group = tables.Column(accessor="tenant_group", verbose_name="Tenant Group", linkify=True)
tenant = tables.Column(accessor="tenant", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
class Meta(NetBoxTable.Meta):
model = SoftwareLicense
fields = (
"pk",
"name",
"description",
"type",
"spdx_expression",
"stored_location",
"start_date",
"expiration_date",
"manufacturer",
"software_product",
"version",
"installation",
"tenant_group",
"tenant",
"support",
"license_amount",
"tags",
)
default_columns = (
"pk",
"name",
"type",
"manufacturer",
"software_product",
"installation",
"tenant_group",
"tenant",
"expiration_date",
"tags",
)
def render_installation(self, **kwargs):
return f"{kwargs['record'].installation.platform}"
+36
View File
@@ -0,0 +1,36 @@
from netbox.plugins import PluginTemplateExtension, get_plugin_config
installations = dict(
device=get_plugin_config("netbox_slm", "link_device_installations"),
cluster=get_plugin_config("netbox_slm", "link_cluster_installations"),
virtualmachine=get_plugin_config("netbox_slm", "link_virtualmachine_installations"),
)
class InstallationsCard(PluginTemplateExtension):
models = ["dcim.device", "virtualization.cluster", "virtualization.virtualmachine"]
def get_object_name(self):
object_model_meta = self.context["object"]._meta # one of the self.models defined above
return object_model_meta.model_name
def render_card(self):
return self.render(
"netbox_slm/installations_card_include.html",
extra_context={
"search_obj": self.get_object_name(),
},
)
def left_page(self):
return self.render_card() if installations[self.get_object_name()] == "left" else ""
def right_page(self):
return self.render_card() if installations[self.get_object_name()] == "right" else ""
def full_width_page(self):
return self.render_card() if installations[self.get_object_name()] == "full" else ""
# auto imported by default PluginConfig.template_extensions = template_content.template_extensions
template_extensions = [InstallationsCard]
@@ -0,0 +1,21 @@
{% load helpers %}
<div class="card">
<h2 class="card-header">
Installations
{% if perms.netbox_slm.add_softwareproductinstallation %}
<div class="card-actions">
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add an installation
</a>
</div>
{% endif %}
</h2>
{% if search_obj == "cluster" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" cluster_id=object.pk %}
{% elif search_obj == "device" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" device_id=object.pk %}
{% elif search_obj == "virtualmachine" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" virtualmachine_id=object.pk %}
{% endif %}
</div>
@@ -0,0 +1,88 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software License
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.type }}</td>
</tr>
<tr>
<th scope="row">SPDX expression</th>
<td>{{ object.spdx_expression }}</td>
</tr>
<tr>
<th scope="row">Software Product</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<td>{{ object.version|linkify }}</td>
</tr>
<tr>
<th scope="row">Installation</th>
<td>{{ object.installation|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant Group</th>
<td>{{ object.tenant_group|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>{{ object.tenant|linkify }}</td>
</tr>
<tr>
<th scope="row">Stored location</th>
{% if object.stored_location_url %}
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
{% else %}
<td>{{ object.stored_location_txt }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">Start date</th>
<td>{{ object.start_date }}</td>
</tr>
<tr>
<th scope="row">Expiration date</th>
<td>{{ object.expiration_date }}</td>
</tr>
<tr>
<th scope="row">Support</th>
<td>{{ object.support }}</td>
</tr>
<tr>
<th scope="row">License amount</th>
<td>{{ object.license_amount }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,76 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>{{ object.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproductversion_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
{{ version }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
{% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,64 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Installation
</h5>
<table class="table table-hover attr-table">
{% if object.device %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device|linkify }}</td>
</tr>
{% elif object.virtualmachine %}
<tr>
<th scope="row">Virtual Machine</th>
<td>{{ object.virtualmachine|linkify }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Cluster</th>
<td>{{ object.cluster|linkify }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Software Product</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<td>{{ object.version|linkify }}</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,100 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Version
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>{{ object.software_product.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">Software Product</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Release date</th>
<td>{{ object.release_date }}</td>
</tr>
<tr>
<th scope="row">Documentation url</th>
{% if object.documentation_url %}
<td><a href="{{ object.documentation_url }}">{{ object.documentation_url }}</a></td>
{% else %}
<td>{{ object.documentation_url }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">End of support</th>
<td>{{ object.end_of_support }}</td>
</tr>
<tr>
<th scope="row">Filename</th>
{% if object.file_link %}
<td><a href="{{ object.file_link }}">{{ object.filename }}</a></td>
{% else %}
<td>{{ object.filename }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">File checksum</th>
<td>{{ object.file_checksum }}</td>
</tr>
<tr>
<th scope="row">Release type</th>
<td>{{ object.get_release_type_display }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
{% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% endfor %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
+66
View File
@@ -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()
+51
View File
@@ -0,0 +1,51 @@
from unittest.mock import patch
from django.contrib.auth import get_user_model
from .base import SlmBaseTestCase
class FunctionalTestCase(SlmBaseTestCase):
"""Functional test cases that require a client and user with permissions"""
def setUp(self):
test_user, _ = get_user_model().objects.get_or_create(username="test", is_superuser=True)
self.client.force_login(test_user)
@classmethod
def tearDownClass(cls):
get_user_model().objects.all().delete()
super().tearDownClass()
def test_template_content(self):
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
self.assertContains(cluster_response, "</span> Add an installation")
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
self.assertContains(device_response, "</span> Add an installation")
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
self.assertTemplateUsed(vm_response, "netbox_slm/installations_card_include.html")
self.assertContains(vm_response, f'<a href="/plugins/slm/installations/add/?virtualmachine={self.vm.pk}"')
def test_setting_skip_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_virtualmachine_installations=None)))
with patch.dict("netbox_slm.template_content.installations", {"virtualmachine": None}):
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
self.assertTemplateNotUsed(vm_response, "netbox_slm/installations_card_include.html")
def test_setting_left_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_device_installations="left")))
with patch.dict("netbox_slm.template_content.installations", {"device": "left"}):
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
self.assertContains(device_response, "</span> Add an installation")
def test_setting_full_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_cluster_installations="full")))
with patch.dict("netbox_slm.template_content.installations", {"cluster": "full"}):
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
self.assertContains(cluster_response, "</span> Add an installation")
+73
View File
@@ -0,0 +1,73 @@
from django.core.exceptions import ValidationError
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_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_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()
+15
View File
@@ -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"))),
]
+32
View File
@@ -0,0 +1,32 @@
from .software_license import (
SoftwareLicenseListView,
SoftwareLicenseView,
SoftwareLicenseEditView,
SoftwareLicenseDeleteView,
SoftwareLicenseBulkDeleteView,
SoftwareLicenseBulkImportView,
)
from .software_product import (
SoftwareProductListView,
SoftwareProductView,
SoftwareProductEditView,
SoftwareProductDeleteView,
SoftwareProductBulkDeleteView,
SoftwareProductBulkImportView,
)
from .software_product_installation import (
SoftwareProductInstallationListView,
SoftwareProductInstallationView,
SoftwareProductInstallationEditView,
SoftwareProductInstallationDeleteView,
SoftwareProductInstallationBulkDeleteView,
SoftwareProductInstallationBulkImportView,
)
from .software_product_version import (
SoftwareProductVersionListView,
SoftwareProductVersionView,
SoftwareProductVersionEditView,
SoftwareProductVersionDeleteView,
SoftwareProductVersionBulkDeleteView,
SoftwareProductVersionBulkImportView,
)
+50
View File
@@ -0,0 +1,50 @@
from netbox.views import generic
from netbox_slm import filtersets, forms, tables
from netbox_slm.models import SoftwareLicense
from utilities.views import register_model_view
@register_model_view(SoftwareLicense, "list", path="", detail=False)
class SoftwareLicenseListView(generic.ObjectListView):
queryset = SoftwareLicense.objects.all()
filterset = filtersets.SoftwareLicenseFilterSet
filterset_form = forms.SoftwareLicenseFilterForm
table = tables.SoftwareLicenseTable
@register_model_view(SoftwareLicense)
class SoftwareLicenseView(generic.ObjectView):
queryset = SoftwareLicense.objects.all()
@register_model_view(SoftwareLicense, "add", detail=False)
@register_model_view(SoftwareLicense, "edit")
class SoftwareLicenseEditView(generic.ObjectEditView):
queryset = SoftwareLicense.objects.all()
form = forms.SoftwareLicenseForm
@register_model_view(SoftwareLicense, "delete")
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
queryset = SoftwareLicense.objects.all()
@register_model_view(SoftwareLicense, "bulk_import", detail=False)
class SoftwareLicenseBulkImportView(generic.BulkImportView):
queryset = SoftwareLicense.objects.all()
model_form = forms.SoftwareLicenseBulkImportForm
@register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False)
class SoftwareLicenseBulkEditView(generic.BulkEditView):
queryset = SoftwareLicense.objects.all()
filterset = filtersets.SoftwareLicenseFilterSet
table = tables.SoftwareLicenseTable
form = forms.SoftwareLicenseBulkEditForm
@register_model_view(SoftwareLicense, "bulk_delete", path="delete", detail=False)
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareLicense.objects.all()
filterset = filtersets.SoftwareLicenseFilterSet
table = tables.SoftwareLicenseTable
+54
View File
@@ -0,0 +1,54 @@
from netbox.views import generic
from netbox_slm import filtersets, forms, tables
from netbox_slm.models import SoftwareProduct
from utilities.views import register_model_view
@register_model_view(SoftwareProduct, "list", path="", detail=False)
class SoftwareProductListView(generic.ObjectListView):
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
@register_model_view(SoftwareProduct)
class SoftwareProductView(generic.ObjectView):
queryset = SoftwareProduct.objects.all()
def get_extra_context(self, request, instance):
versions = instance.softwareproductversion_set.all()
return {"versions": versions}
@register_model_view(SoftwareProduct, "add", detail=False)
@register_model_view(SoftwareProduct, "edit")
class SoftwareProductEditView(generic.ObjectEditView):
queryset = SoftwareProduct.objects.all()
form = forms.SoftwareProductForm
@register_model_view(SoftwareProduct, "delete")
class SoftwareProductDeleteView(generic.ObjectDeleteView):
queryset = SoftwareProduct.objects.all()
@register_model_view(SoftwareProduct, "bulk_import", detail=False)
class SoftwareProductBulkImportView(generic.BulkImportView):
queryset = SoftwareProduct.objects.all()
model_form = forms.SoftwareProductBulkImportForm
@register_model_view(SoftwareProduct, "bulk_edit", path="edit", detail=False)
class SoftwareProductBulkEditView(generic.BulkEditView):
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
table = tables.SoftwareProductTable
form = forms.SoftwareProductBulkEditForm
@register_model_view(SoftwareProduct, "bulk_delete", path="delete", detail=False)
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProduct.objects.all()
filterset = filtersets.SoftwareProductFilterSet
table = tables.SoftwareProductTable
@@ -0,0 +1,50 @@
from netbox.views import generic
from netbox_slm import filtersets, forms, tables
from netbox_slm.models import SoftwareProductInstallation
from utilities.views import register_model_view
@register_model_view(SoftwareProductInstallation, "list", path="", detail=False)
class SoftwareProductInstallationListView(generic.ObjectListView):
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
filterset_form = forms.SoftwareProductInstallationFilterForm
table = tables.SoftwareProductInstallationTable
@register_model_view(SoftwareProductInstallation)
class SoftwareProductInstallationView(generic.ObjectView):
queryset = SoftwareProductInstallation.objects.all()
@register_model_view(SoftwareProductInstallation, "add", detail=False)
@register_model_view(SoftwareProductInstallation, "edit")
class SoftwareProductInstallationEditView(generic.ObjectEditView):
queryset = SoftwareProductInstallation.objects.all()
form = forms.SoftwareProductInstallationForm
@register_model_view(SoftwareProductInstallation, "delete")
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
queryset = SoftwareProductInstallation.objects.all()
@register_model_view(SoftwareProductInstallation, "bulk_import", detail=False)
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
queryset = SoftwareProductInstallation.objects.all()
model_form = forms.SoftwareProductInstallationBulkImportForm
@register_model_view(SoftwareProductInstallation, "bulk_edit", path="edit", detail=False)
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
table = tables.SoftwareProductInstallationTable
form = forms.SoftwareProductInstallationBulkEditForm
@register_model_view(SoftwareProductInstallation, "bulk_delete", path="delete", detail=False)
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductInstallation.objects.all()
filterset = filtersets.SoftwareProductInstallationFilterSet
table = tables.SoftwareProductInstallationTable
@@ -0,0 +1,54 @@
from netbox.views import generic
from netbox_slm import filtersets, forms, tables
from netbox_slm.models import SoftwareProductVersion
from utilities.views import register_model_view
@register_model_view(SoftwareProductVersion, "list", path="", detail=False)
class SoftwareProductVersionListView(generic.ObjectListView):
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
filterset_form = forms.SoftwareProductVersionFilterForm
table = tables.SoftwareProductVersionTable
@register_model_view(SoftwareProductVersion)
class SoftwareProductVersionView(generic.ObjectView):
queryset = SoftwareProductVersion.objects.all()
def get_extra_context(self, request, instance):
installation_count = instance.get_installation_count()
return {"installations": installation_count}
@register_model_view(SoftwareProductVersion, "add", detail=False)
@register_model_view(SoftwareProductVersion, "edit")
class SoftwareProductVersionEditView(generic.ObjectEditView):
queryset = SoftwareProductVersion.objects.all()
form = forms.SoftwareProductVersionForm
@register_model_view(SoftwareProductVersion, "delete")
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
queryset = SoftwareProductVersion.objects.all()
@register_model_view(SoftwareProductVersion, "bulk_import", detail=False)
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
queryset = SoftwareProductVersion.objects.all()
model_form = forms.SoftwareProductVersionBulkImportForm
@register_model_view(SoftwareProductVersion, "bulk_edit", path="edit", detail=False)
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
table = tables.SoftwareProductVersionTable
form = forms.SoftwareProductVersionBulkEditForm
@register_model_view(SoftwareProductVersion, "bulk_delete", path="delete", detail=False)
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductVersion.objects.all()
filterset = filtersets.SoftwareProductVersionFilterSet
table = tables.SoftwareProductVersionTable
+84
View File
@@ -0,0 +1,84 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "netbox-slm"
dynamic = ["version"]
description = "Software Lifecycle Management Netbox Plugin"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
authors = [
{name = "ICTU", email = "open-source-projects@ictu.nl"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
dependencies = [
"license-expression == 30.4.4",
]
[project.optional-dependencies]
build = [
"build == 1.5.0",
"setuptools == 82.0.1",
"twine == 6.2.0",
]
ci = [
"coverage == 7.14.3",
"ruff == 0.15.19",
]
[project.urls]
Homepage = "https://github.com/ICTU/netbox_slm/"
Changelog = "https://github.com/ICTU/netbox_slm/blob/master/CHANGELOG.md"
Issues = "https://github.com/ICTU/netbox_slm/issues/"
[tool.setuptools.dynamic]
version = {attr = "netbox_slm.__version__"}
[tool.setuptools.packages.find]
include = ["netbox_slm"]
[tool.ruff]
line-length = 120
target-version = "py312"
src = ["netbox_slm"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files
"F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files
]
"netbox_slm/migrations/*.py" = [
"E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files
]
[tool.ruff.format]
exclude = ["netbox_slm/migrations/*.py"]
+14
View File
@@ -0,0 +1,14 @@
# Project metadata
sonar.organization=ictu
sonar.projectKey=ICTU_netbox_slm
sonar.projectName=netbox-slm
sonar.projectVersion=1.9.0
# Path is relative to the sonar-project.properties file
sonar.sources=netbox_slm
sonar.exclusions=netbox_slm/tests/**
sonar.tests=netbox_slm/tests
sonar.python.version=3.11, 3.12, 3.13, 3.14
# Python test reports
sonar.python.coverage.reportPaths=/ci/reports/coverage.xml
-13
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
include ../LICENSE
include ../README.rst
recursive-include netbox_slm/templates *
-222
View File
@@ -1,222 +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.9`` 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
::
$ 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.
Gotchas
~~~~~~~~
*Netboxs 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. Its
recommended to often check the runtime server or patch Netboxs 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 theres no processes
using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand:
::
$ cd netbox-docker
$ docker-compose up -d
or stop the stack with
$ docker-compose down
# to start fresh:
$ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate
this will require you to re-run the migrate commando's for netbox-slm, see further down below
Go back to the netbox configuration.py file and update the postgres and
redis connection strings (username, password) to the ones the netbox
docker backend is using, for example (using default user and passwords
from the netbox docker example):
::
<<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
<<collapsed>>
Now you can run commands from the netbox repository like this;
::
$ cd netbox/netbox
$ export PYTHONPATH=../../netbox-slm/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.
-17
View File
@@ -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.93'
author = 'Hedde van der Heide'
author_email = 'hedde.vanderheide@ictu.nl'
base_url = 'slm'
required_settings = []
default_settings = {
'version_info': False
}
config = SLMConfig
-14
View File
@@ -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,)
-40
View File
@@ -1,40 +0,0 @@
from rest_framework import serializers
from netbox.api.serializers import PrimaryModelSerializer, OrganizationalModelSerializer
# from netbox_slm.api.nested_serializers import (
# NestedRecordSerializer,
# NestedNameServerSerializer,
# )
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
class SoftwareProductSerializer(PrimaryModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
)
class Meta:
model = SoftwareProduct
fields = [
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
class SoftwareProductVersionSerializer(PrimaryModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
class Meta:
model = SoftwareProductVersion
fields = [
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
-13
View File
@@ -1,13 +0,0 @@
from netbox.api import OrderedDefaultRouter
from netbox_slm.api.views import (
NetboxSLMRootView,
SoftwareProductViewSet,
SoftwareProductVersionViewSet,
)
router = OrderedDefaultRouter()
router.APIRootView = NetboxSLMRootView
router.register("softwareproducts", SoftwareProductViewSet)
router.register("softwareproductversions", SoftwareProductVersionViewSet)
urlpatterns = router.urls
-44
View File
@@ -1,44 +0,0 @@
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from extras.api.views import CustomFieldModelViewSet
from netbox_slm.api.serializers import SoftwareProductSerializer, SoftwareProductVersionSerializer
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
class NetboxSLMRootView(APIRootView):
"""
NetboxSLM API root view
"""
def get_view_name(self):
return "NetboxSLM"
class SoftwareProductViewSet(CustomFieldModelViewSet):
queryset = SoftwareProduct.objects.all()
serializer_class = SoftwareProductSerializer
filterset_class = SoftwareProductFilter
# @action(detail=True, methods=["get"])
# def records(self, request, pk=None):
# records = Record.objects.filter(zone=pk)
# serializer = RecordSerializer(records, many=True, context={"request": request})
# return Response(serializer.data)
class SoftwareProductVersionViewSet(CustomFieldModelViewSet):
queryset = SoftwareProductVersion.objects.all()
serializer_class = SoftwareProductVersionSerializer
filterset_class = SoftwareProductVersionFilter
# @action(detail=True, methods=["get"])
# def records(self, request, pk=None):
# records = Record.objects.filter(zone=pk)
# serializer = RecordSerializer(records, many=True, context={"request": request})
# return Response(serializer.data)
# for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py
-50
View File
@@ -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
-75
View File
@@ -1,75 +0,0 @@
import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from extras.filters import TagFilter
from netbox.filtersets import PrimaryModelFilterSet
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import DynamicModelMultipleChoiceField
class BaseFilter(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method="search",
label="Search",
)
class SoftwareProductFilter(BaseFilter):
"""Filter capabilities for SoftwareProduct instances."""
name = django_filters.CharFilter(
lookup_expr="icontains",
)
# tag = TagFilter()
class Meta:
model = SoftwareProduct
fields = ("name",) # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductVersionFilter(BaseFilter):
name = django_filters.CharFilter(
lookup_expr="icontains", label="Version"
)
# software_product_id = django_filters.ModelMultipleChoiceFilter(
# queryset=SoftwareProduct.objects.all(),
# label='SoftwareProduct (ID)',
# )
# software_product = django_filters.ModelMultipleChoiceFilter(
# field_name='software_product__name',
# queryset=SoftwareProduct.objects.all(),
# to_field_name='name',
# label='SoftwareProduct (name)',
# )
# tag = TagFilter()
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product") # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductInstallationFilter(BaseFilter):
class Meta:
model = SoftwareProductInstallation
fields = tuple() # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(software_product__name__icontains=value) | Q(version__name__icontains=value)
return queryset.filter(qs_filter)
-202
View File
@@ -1,202 +0,0 @@
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from extras.forms import (
CustomFieldModelForm,
CustomFieldModelCSVForm,
AddRemoveTagsForm,
CustomFieldModelBulkEditForm,
CustomFieldModelFilterForm,
)
from dcim.models import Manufacturer, Device
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import (
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
)
from virtualization.models import VirtualMachine
class SoftwareProductForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProduct object."""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer", "description",) # "tags")
class SoftwareProductFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProduct instances."""
model = SoftwareProduct
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
)
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name",)
class SoftwareProductBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
class SoftwareProductVersionForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProductVersion object."""
name = forms.CharField(label=_("Version"))
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product",) # "tags")
# def clean(self):
# import pdb;pdb.set_trace()
# return super(SoftwareProductVersionForm, self).clean()
class SoftwareProductVersionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProductVersion instances."""
model = SoftwareProductVersion
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
)
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductVersionCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProductVersion
fields = ("name",)
class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
class SoftwareProductInstallationForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={
'software_product': '$software_product',
}
)
# todo need version and device ?
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version",) # "tags")
def clean(self):
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(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProductInstallation instances."""
model = SoftwareProductInstallation
q = forms.CharField(required=False, label="Search")
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductInstallationCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProductInstallation
fields = tuple()
class SoftwareProductInstallationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
View File
-98
View File
@@ -1,98 +0,0 @@
from django.db import models
from django.urls import reverse, reverse_lazy
from django.utils import safestring
from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProduct(PrimaryModel):
"""
A SoftwareProduct represents ...
"""
name = models.CharField(max_length=128)
description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
related_name='software_products',
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(PrimaryModel):
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT,
related_name='softwareproduct_versions'
)
name = models.CharField(max_length=64)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk})
def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
count=count
)) if count else "0"
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProductInstallation(PrimaryModel):
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.PROTECT,
related_name='softwareproduct_installations',
null=True,
blank=True
)
virtualmachine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.PROTECT,
related_name='softwareproduct_installations',
null = True,
blank = True
)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT,
related_name='software_products'
)
version = models.ForeignKey(
to='netbox_slm.SoftwareProductVersion',
on_delete=models.PROTECT,
related_name='softwareproduct_versions'
)
objects = RestrictedQuerySet.as_manager()
def __str__(self):
return f"{self.pk}"
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
-65
View File
@@ -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"],
),
)
),
)
View File
-125
View File
@@ -1,125 +0,0 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn
class SoftwareProductTable(BaseTable):
"""Table for displaying SoftwareProduct objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
manufacturer = tables.Column(
accessor=Accessor('manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
# tags = TagColumn(
# url_name="plugins:netbox_dns:zone_list",
# )
class Meta(BaseTable.Meta):
model = SoftwareProduct
fields = (
"pk",
"name",
"manufacturer",
"description",
"installations",
# "tags",
)
default_columns = (
"pk",
"name",
"manufacturer",
"description",
"installations",
# "tags",
)
class SoftwareProductVersionTable(BaseTable):
"""Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Version')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
manufacturer = tables.Column(
accessor=Accessor('software_product__manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
# tags = TagColumn(
# url_name="plugins:netbox_dns:zone_list",
# )
class Meta(BaseTable.Meta):
model = SoftwareProductVersion
fields = (
"pk",
"name",
"software_product",
"manufacturer",
"installations",
# "tags",
)
default_columns = (
"pk",
"name",
"software_product",
"manufacturer",
"installations",
# "tags",
)
class SoftwareProductInstallationTable(BaseTable):
"""Table for displaying SoftwareProductInstallation objects."""
pk = ToggleColumn()
name = tables.LinkColumn()
device = tables.Column(
accessor=Accessor('device'),
linkify=True
)
virtualmachine = tables.Column(
accessor=Accessor('virtualmachine'),
linkify=True
)
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
version = tables.Column(
accessor=Accessor('version'),
linkify=True
)
# tags = TagColumn(
# url_name="plugins:netbox_dns:zone_list",
# )
class Meta(BaseTable.Meta):
model = SoftwareProductInstallation
fields = (
"pk",
"name",
"device",
"virtualmachine",
"software_product",
"version",
# "tags",
)
default_columns = (
"pk",
"device",
"virtualmachine",
"software_product",
"version",
# "tags",
)
View File
@@ -1,93 +0,0 @@
{% extends 'base/layout.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load view_helpers %}
{% block header %}
{# Breadcrumbs #}
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
<ol class="breadcrumb">
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url object|plugin_viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
{% endblock %}
</ol>
</nav>
{{ block.super }}
{% endblock %}
{% block title %}{{ object }}{% endblock %}
{% block subtitle %}
<div class="object-subtitle">
<span>Created {{ object.created|annotated_date }}</span>
<span class="separator">&middot;</span>
<span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
</div>
{% endblock %}
{% block controls %}
{# Clone/Edit/Delete Buttons #}
<div class="controls">
<div class="control-group">
{% plugin_buttons object %}
{# Extra buttons #}
{% block extra_controls %}{% endblock %}
{% if object.clone_fields and request.user|can_add:object %}
{% plugin_clone_button object %}
{% endif %}
{% if request.user|can_change:object %}
{% plugin_edit_button object %}
{% endif %}
{% if request.user|can_delete:object %}
{% plugin_delete_button object %}
{% endif %}
</div>
<div class="control-group">
{% custom_links object %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
</li>
{% endblock tab_items %}
{% if perms.extras.view_journalentry %}
{% with journal_viewname=object|plugin_viewname:'journal' %}
{% url journal_viewname pk=object.pk as journal_url %}
{% if journal_url %}
<li role="presentation" class="nav-item">
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
Journal {% badge object.journal_entries.count %}
</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
{% if perms.extras.view_objectchange %}
{% with changelog_viewname=object|plugin_viewname:'changelog' %}
{% url changelog_viewname pk=object.pk as changelog_url %}
{% if changelog_url %}
<li role="presentation" class="nav-item">
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{% block content %}{% endblock %}
</div>
{% endblock %}
@@ -1,141 +0,0 @@
{% extends 'base/layout.html' %}
{% load buttons %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load static %}
{% load view_helpers %}
{% block controls %}
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
{% if permissions.add and 'add' in action_buttons %}
{% add_button content_type.model_class|plugin_viewname:"add" %}
{% endif %}
{% if permissions.add and 'import' in action_buttons %}
{% import_button content_type.model_class|plugin_viewname:"import" %}
{% endif %}
{% if 'export' in action_buttons %}
{% export_button content_type %}
{% endif %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
{% badge table.page.paginator.count %}
</button>
</li>
{% if filter_form %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
Filters
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
</button>
</li>
{% endif %}
{% endblock tab_items %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{# Object list #}
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
{# Applied filters #}
{% if filter_form %}
{% applied_filters filter_form request.GET %}
{% endif %}
{# "Select all" form #}
{% if table.paginator.num_pages > 1 %}
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
<div id="select-all-box" class="d-none card noprint">
<form method="post" class="form col-md-12">
{% csrf_token %}
<div class="card-body">
<div class="float-end">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
<div class="form-check">
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
<label for="select-all" class="form-check-label">
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
</div>
</form>
</div>
{% endwith %}
{% endif %}
{# Object table controls #}
{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{# Object table #}
<div class="card">
<div class="card-body">
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
</div>
</div>
{# Form buttons #}
{% if permissions.change or permissions.delete %}
{% with bulk_edit_url=content_type.model_class|plugin_validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|plugin_validated_viewname:"bulk_delete" %}
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
</button>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</form>
{# Paginator #}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
{# Filter form #}
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
{% endif %}
</div>
{# Table config form #}
{% table_config_form table table_name="ObjectTable" %}
{% endblock content-wrapper %}
@@ -1,76 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproduct_versions.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
</a>
{% endfor %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Versions</h5>
<div class="card-body">
<table class="table table-hover attr-table table-striped">
<thead>
<tr>
<th scope="col">NAME</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>{{ version.name|truncatechars:32 }}</td>
<td class="noprint text-end text-nowrap">
{% if request.user|can_change:object %}
{% plugin_edit_button version %}
{% endif %}
{% if request.user|can_delete:object %}
{% plugin_delete_button version %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -1,82 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Installation</th>
<td>{{ object }}</td>
</tr>
<tr>
<th scope="row">bla</th>
<td>
<!-- {% for version in object.softwareproduct_versions.all %}-->
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
<!-- </a>-->
<!-- {% endfor %}-->
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,82 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
<!-- {% for version in object.softwareproduct_versions.all %}-->
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
<!-- </a>-->
<!-- {% endfor %}-->
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,91 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{% comment %}
<a href="{% url 'plugins:netbox_slm:record_add' %}?zone={{ object.id }}" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-plus-thick"></i> Add Record
</a>
{% endcomment %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Zone
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
<ul>
<!-- {% for nameserver in object.nameservers.all %}-->
<!-- <li>-->
<!-- <a href="{% url 'plugins:netbox_slm:nameserver' pk=nameserver.pk %}">-->
<!-- {{ nameserver }}-->
<!-- </a>-->
<!-- </li>-->
<!-- {% endfor %}-->
</ul>
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,94 +0,0 @@
from django import template
from django.urls import NoReverseMatch, reverse
from django import template
from utilities.templatetags.buttons import _get_viewname
from utilities.utils import prepare_cloned_fields
register = template.Library()
@register.filter()
def plugin_validated_viewname(model, action):
"""Return the view name for :
* the given model and action if valid
* or the given model and action if valid inside a plugin
* or None if invalid.
"""
core_viewname = f"{model._meta.app_label}:{model._meta.model_name}_{action}"
plugin_viewname = (
f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
)
try:
# Validate and return the view name. We don't return the actual URL yet because many of the templates
# are written to pass a name to {% url %}.
reverse(core_viewname)
return core_viewname
except NoReverseMatch:
try:
reverse(plugin_viewname)
return plugin_viewname
except NoReverseMatch:
return None
@register.filter()
def plugin_viewname(model, action):
"""
Return the view name for the given model and action. Does not perform any validation.
"""
return f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
#
# Instance buttons
#
@register.inclusion_tag("buttons/clone.html")
def plugin_clone_button(instance):
viewname = _get_viewname(instance, "add")
url = reverse(f"plugins:{viewname}")
# Populate cloned field values
param_string = prepare_cloned_fields(instance)
if param_string:
url = f"{url}?{param_string}"
return {
"url": url,
}
@register.inclusion_tag("buttons/edit.html")
def plugin_edit_button(instance, **kwargs):
viewname = _get_viewname(instance, "edit")
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
return {
"url": url,
}
@register.inclusion_tag("buttons/delete.html")
def plugin_delete_button(instance, **kwargs):
viewname = _get_viewname(instance, "delete")
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
return {
"url": url,
}
#
# List buttons
#
@register.inclusion_tag("buttons/add.html")
def plugin_add_button(url):
url = reverse(f"plugins:{url}")
return {
"add_url": url,
}
-54
View File
@@ -1,54 +0,0 @@
from django.urls import path
from extras.views import ObjectChangeLogView
from netbox_slm import views
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
urlpatterns = [
# Software Products
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"),
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"),
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
path(
"software-products/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproduct_changelog",
kwargs={"model": SoftwareProduct},
),
# Software Product Versions
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), name="softwareproductversion_bulk_delete"),
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path(
"versions/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductversion_changelog",
kwargs={"model": SoftwareProductVersion},
),
# Software Product Versions
path("installations/", views.SoftwareProductInstallationListView.as_view(), name='softwareproductinstallation_list'),
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), name='softwareproductinstallation_add'),
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), name="softwareproductinstallation_import"),
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), name="softwareproductinstallation_bulk_edit"),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_bulk_delete"),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(), name="softwareproductinstallation"),
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(), name="softwareproductinstallation_delete"),
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(), name="softwareproductinstallation_edit"),
path(
"installations/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductinstallation_changelog",
kwargs={"model": SoftwareProductInstallation},
),
]
-165
View File
@@ -1,165 +0,0 @@
from netbox.views import generic
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter
from netbox_slm.forms import (
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm,
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
)
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
)
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
class SoftwareProductListView(generic.ObjectListView):
"""View for listing all existing SoftwareProducts."""
queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter
filterset_form = SoftwareProductFilterForm
table = SoftwareProductTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductView(generic.ObjectView):
"""Display SoftwareProduct details"""
queryset = SoftwareProduct.objects.all()
def get_extra_context(self, request, instance):
versions = instance.softwareproduct_versions.all()
return {"versions": versions}
class SoftwareProductEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProduct instance."""
queryset = SoftwareProduct.objects.all()
model_form = SoftwareProductForm
class SoftwareProductDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProduct instance"""
queryset = SoftwareProduct.objects.all()
class SoftwareProductBulkImportView(generic.BulkImportView):
queryset = SoftwareProduct.objects.all()
model_form = SoftwareProductCSVForm
table = SoftwareProductTable
class SoftwareProductBulkEditView(generic.BulkEditView):
queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter
table = SoftwareProductTable
form = SoftwareProductBulkEditForm
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProduct.objects.all()
table = SoftwareProductTable
class SoftwareProductVersionListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductVersions."""
queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter
filterset_form = SoftwareProductVersionFilterForm
table = SoftwareProductVersionTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductVersionView(generic.ObjectView):
"""Display SoftwareProductVersion details"""
queryset = SoftwareProductVersion.objects.all()
# def get_extra_context(self, request, instance):
# records = instance.record_set.all()
# return {"records": records}
class SoftwareProductVersionEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductVersion instance."""
queryset = SoftwareProductVersion.objects.all()
model_form = SoftwareProductVersionForm
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProductVersion instance"""
queryset = SoftwareProductVersion.objects.all()
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
queryset = SoftwareProductVersion.objects.all()
model_form = SoftwareProductVersionCSVForm
table = SoftwareProductVersionTable
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter
table = SoftwareProductVersionTable
form = SoftwareProductVersionBulkEditForm
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductVersion.objects.all()
table = SoftwareProductVersionTable
class SoftwareProductInstallationListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductInstallations."""
queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter
filterset_form = SoftwareProductInstallationFilterForm
table = SoftwareProductInstallationTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductInstallationView(generic.ObjectView):
"""Display SoftwareProductInstallation details"""
queryset = SoftwareProductVersion.objects.all()
# def get_extra_context(self, request, instance):
# records = instance.record_set.all()
# return {"records": records}
class SoftwareProductInstallationEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductInstallation instance."""
queryset = SoftwareProductInstallation.objects.all()
model_form = SoftwareProductInstallationForm
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
"""View for deleting a SoftwareProductInstallation instance"""
queryset = SoftwareProductInstallation.objects.all()
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
queryset = SoftwareProductInstallation.objects.all()
model_form = SoftwareProductInstallationCSVForm
table = SoftwareProductInstallationTable
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter
table = SoftwareProductInstallationTable
form = SoftwareProductInstallationBulkEditForm
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductInstallation.objects.all()
table = SoftwareProductInstallationTable
-29
View File
@@ -1,29 +0,0 @@
[metadata]
name = netbox-slm
version = 0.93
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.6
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.6
-3
View File
@@ -1,3 +0,0 @@
from setuptools import setup, Extension
setup()
-17
View File
@@ -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