3 Commits
Author SHA1 Message Date
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
30 changed files with 644 additions and 582 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Python
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.0.0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install tools
run: pip install .[tools]
- name: Run ruff
run: ruff netbox_slm
- name: Run black
run: black netbox_slm
+1
View File
@@ -10,3 +10,4 @@ ci/docker-compose.override.yml
ci/reports ci/reports
out/production out/production
*.iml *.iml
build/
-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"
}
}
}
+4 -7
View File
@@ -27,8 +27,8 @@ Next, follow these instructions (based on the Netbox docker variant
to install the Netbox SLM plugin: to install the Netbox SLM plugin:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in 1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/extra.py``. ``configuration/plugins.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.2`` as 2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
contents. contents.
3. Create a ``Dockerfile-SLM`` with contents: 3. Create a ``Dockerfile-SLM`` with contents:
@@ -65,20 +65,17 @@ And finally, run Netbox with the SLM plugin: ``docker compose up``
To draft a release; To draft a release;
update the setup.py file to reflect the new version, then from the *src* update the `netbox_slm/__init__.py` file to reflect the new version, then from the *src*
directory run directory run
``` ```
# make sure to update the version in netbox_slm/__init__.py $ python -m build
$ python setup.py sdist
$ twine upload dist/* $ twine upload dist/*
``` ```
On Github.com create a similar tag and version. These steps could be On Github.com create a similar tag and version. These steps could be
automated with a github workflow. 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) ## Developer Guide (local installation)
+3 -2
View File
@@ -7,10 +7,11 @@ ENV PATH="/opt/netbox/venv/bin:$PATH"
COPY ../ /build COPY ../ /build
RUN python setup.py sdist RUN pip install -U .[build]
RUN python -m build
RUN pip install --no-index /build RUN pip install --no-index /build
FROM netboxcommunity/netbox:v3.4.7 FROM netboxcommunity/netbox:v3.6.1
ARG PYTHON_VERSION=3.10 ARG PYTHON_VERSION=3.10
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM netboxcommunity/netbox:v3.4.7 FROM netboxcommunity/netbox:v3.6.1
RUN mkdir /ci && chmod go+w /ci RUN mkdir /ci && chmod go+w /ci
COPY ../ci/requirements_ci.txt /ci/ COPY ../ci/requirements_ci.txt /ci/
+1 -1
View File
@@ -1,6 +1,6 @@
PLUGINS = ['netbox_slm'] PLUGINS = ['netbox_slm']
DEBUG = True DEBUG = True
SECRET_KEY = 'dummy' SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
DEVELOPER = True DEVELOPER = True
PLUGINS_CONFIG = { PLUGINS_CONFIG = {
'netbox_slm': dict( 'netbox_slm': dict(
+1 -1
View File
@@ -1,7 +1,7 @@
version: '3.7' version: '3.7'
services: services:
netbox: &netbox netbox: &netbox
image: netboxcommunity/netbox:${VERSION-v3.3-2.2.0} image: netboxcommunity/netbox:${VERSION-v3.6.1}
depends_on: depends_on:
- postgres - postgres
- redis - redis
+10 -10
View File
@@ -1,18 +1,18 @@
from extras.plugins import PluginConfig from extras.plugins import PluginConfig
__version__ = "1.5"
class SLMConfig(PluginConfig): class SLMConfig(PluginConfig):
name = 'netbox_slm' name = "netbox_slm"
verbose_name = 'Software Lifecycle Management' verbose_name = "Software Lifecycle Management"
description = 'Software Lifecycle Management Netbox Plugin.' description = "Software Lifecycle Management Netbox Plugin."
version = '1.4' version = __version__
author = 'ICTU' author = "ICTU"
author_email = 'open-source-projects@ictu.nl' author_email = "open-source-projects@ictu.nl"
base_url = 'slm' base_url = "slm"
required_settings = [] required_settings = []
default_settings = { default_settings = {"version_info": False}
'version_info': False
}
config = SLMConfig config = SLMConfig
+48 -18
View File
@@ -6,15 +6,19 @@ from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareP
class SoftwareProductSerializer(NetBoxModelSerializer): class SoftwareProductSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField() display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail")
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
)
class Meta: class Meta:
model = SoftwareProduct model = SoftwareProduct
fields = [ fields = [
'id', 'display', 'url', 'name', "id",
'tags', 'custom_field_data', 'created', 'last_updated', "display",
"url",
"name",
"tags",
"custom_field_data",
"created",
"last_updated",
] ]
def get_display(self, obj): def get_display(self, obj):
@@ -23,15 +27,20 @@ class SoftwareProductSerializer(NetBoxModelSerializer):
class SoftwareProductVersionSerializer(NetBoxModelSerializer): class SoftwareProductVersionSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField() display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail")
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
class Meta: class Meta:
model = SoftwareProductVersion model = SoftwareProductVersion
fields = [ fields = [
'id', 'display', 'url', 'name', 'software_product', "id",
'tags', 'custom_field_data', 'created', 'last_updated', "display",
"url",
"name",
"software_product",
"tags",
"custom_field_data",
"created",
"last_updated",
] ]
def get_display(self, obj): def get_display(self, obj):
@@ -47,8 +56,18 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = SoftwareProductInstallation model = SoftwareProductInstallation
fields = [ fields = [
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version', "id",
'tags', 'custom_field_data', 'created', 'last_updated', "display",
"url",
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
"custom_field_data",
"created",
"last_updated",
] ]
def get_display(self, obj): def get_display(self, obj):
@@ -57,16 +76,27 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
class SoftwareLicenseSerializer(NetBoxModelSerializer): class SoftwareLicenseSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField() display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
view_name="plugins-api:netbox_slm-api:softwarelicense-detail"
)
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
fields = [ fields = [
'id', 'display', 'url', 'name', 'description', 'type', 'stored_location', "id",
'start_date', 'expiration_date', 'software_product', 'version', 'installation', "display",
'tags', 'custom_field_data', 'created', 'last_updated', "url",
"name",
"description",
"type",
"stored_location",
"start_date",
"expiration_date",
"software_product",
"version",
"installation",
"tags",
"custom_field_data",
"created",
"last_updated",
] ]
def get_display(self, obj): def get_display(self, obj):
+20 -17
View File
@@ -24,17 +24,17 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = SoftwareProductVersion model = SoftwareProductVersion
fields = ( fields = ("software_product",)
"software_product",
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
"""Perform the filtered search.""" """Perform the filtered search."""
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = Q(name__icontains=value) | \ qs_filter = (
Q(software_product__name__icontains=value) | \ Q(name__icontains=value)
Q(software_product__manufacturer__name__icontains=value) | Q(software_product__name__icontains=value)
| Q(software_product__manufacturer__name__icontains=value)
)
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
@@ -43,17 +43,17 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = SoftwareProductInstallation model = SoftwareProductInstallation
fields = ( fields = ("software_product",)
"software_product",
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
"""Perform the filtered search.""" """Perform the filtered search."""
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = Q(software_product__name__icontains=value) | \ qs_filter = (
Q(software_product__manufacturer__name__icontains=value) | \ Q(software_product__name__icontains=value)
Q(version__name__icontains=value) | Q(software_product__manufacturer__name__icontains=value)
| Q(version__name__icontains=value)
)
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
@@ -68,9 +68,12 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
"""Perform the filtered search.""" """Perform the filtered search."""
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = Q(name__icontains=value) | \ qs_filter = (
Q(software_product__name__icontains=value) | \ Q(name__icontains=value)
Q(version__name__icontains=value) | \ | Q(software_product__name__icontains=value)
Q(installation__device__name__icontains=value) | \ | Q(version__name__icontains=value)
Q(installation__virtualmachine__name__icontains=value) | Q(installation__device__name__icontains=value)
| Q(installation__virtualmachine__name__icontains=value)
| Q(installation__cluster__name__icontains=value)
)
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
+24 -4
View File
@@ -1,4 +1,24 @@
from .software_license import * from .software_license import (
from .software_product import * SoftwareLicenseForm,
from .software_product_installation import * SoftwareLicenseFilterForm,
from .software_product_version import * SoftwareLicenseImportForm,
SoftwareLicenseBulkEditForm,
)
from .software_product import (
SoftwareProductForm,
SoftwareProductFilterForm,
SoftwareProductImportForm,
SoftwareProductBulkEditForm,
)
from .software_product_installation import (
SoftwareProductInstallationForm,
SoftwareProductInstallationFilterForm,
SoftwareProductInstallationImportForm,
SoftwareProductInstallationBulkEditForm,
)
from .software_product_version import (
SoftwareProductVersionForm,
SoftwareProductVersionFilterForm,
SoftwareProductVersionImportForm,
SoftwareProductVersionBulkEditForm,
)
+54 -28
View File
@@ -1,29 +1,30 @@
from django.forms import DateField
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, TagFilterField, LaxURLField
from utilities.forms.widgets import APISelect, DatePicker
class SoftwareLicenseForm(NetBoxModelForm): class SoftwareLicenseForm(NetBoxModelForm):
"""Form for creating a new SoftwareLicense object.""" """Form for creating a new SoftwareLicense object."""
stored_location_url = LaxURLField(required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
widget=APISelect( widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={ query_params={
'software_product': '$software_product', "software_product": "$software_product",
} },
) )
installation = DynamicModelChoiceField( installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(), queryset=SoftwareProductInstallation.objects.all(),
@@ -32,20 +33,39 @@ class SoftwareLicenseForm(NetBoxModelForm):
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")} attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
), ),
query_params={ query_params={
'software_product': '$software_product', "software_product": "$software_product",
} },
) )
start_date = DateField(label=_("Start date"), required=False, widget=DatePicker())
expiration_date = DateField(label=_("Expiration date"), required=False, widget=DatePicker())
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
fields = ("name", "description", "type", "stored_location", "start_date", "expiration_date", fields = (
"software_product", "version", "installation", "tags") "name",
"description",
"type",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"software_product",
"version",
"installation",
"tags",
)
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
model = SoftwareLicense model = SoftwareLicense
fieldsets = ( fieldsets = (
(None, ('q', 'tag',)), (
None,
(
"q",
"tag",
),
),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -54,23 +74,29 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
class SoftwareLicenseImportForm(NetBoxModelImportForm): class SoftwareLicenseImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
fields = ("name", "description", "type", "stored_location", "start_date", "expiration_date",) fields = (
"name",
"description",
"type",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
)
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
queryset=SoftwareProduct.objects.all(), version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
required=False installation = DynamicModelChoiceField(queryset=SoftwareProductInstallation.objects.all(), required=False)
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
required=False
)
model = SoftwareLicense model = SoftwareLicense
fieldsets = ( fieldsets = (
(None, ('software_product', 'version', 'installation',)), (
None,
(
"software_product",
"version",
"installation",
),
),
) )
+6 -5
View File
@@ -3,7 +3,7 @@ from django import forms
from dcim.models import Manufacturer from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProduct from netbox_slm.models import SoftwareProduct
from utilities.forms import DynamicModelChoiceField, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
class SoftwareProductForm(NetBoxModelForm): class SoftwareProductForm(NetBoxModelForm):
@@ -21,9 +21,7 @@ class SoftwareProductForm(NetBoxModelForm):
class SoftwareProductFilterForm(NetBoxModelFilterSetForm): class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct model = SoftwareProduct
fieldsets = ( fieldsets = ((None, ("q", "tag")),)
(None, ('q', 'tag')),
)
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -31,7 +29,10 @@ class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
class SoftwareProductImportForm(NetBoxModelImportForm): class SoftwareProductImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = SoftwareProduct model = SoftwareProduct
fields = ("name", "manufacturer",) fields = (
"name",
"manufacturer",
)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm): class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
@@ -5,61 +5,58 @@ from django.utils.translation import gettext as _
from dcim.models import Device from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from virtualization.models import VirtualMachine from utilities.forms.widgets import APISelect
from virtualization.models import VirtualMachine, Cluster
class SoftwareProductInstallationForm(NetBoxModelForm): class SoftwareProductInstallationForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductInstallation object.""" """Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField( device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
queryset=Device.objects.all(), virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
required=False, cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
widget=APISelect( widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
required=True, required=True,
widget=APISelect( widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={ query_params={
'software_product': '$software_product', "software_product": "$software_product",
} },
) )
class Meta: class Meta:
model = SoftwareProductInstallation model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version", "tags") fields = ("device", "virtualmachine", "cluster", "software_product", "version", "tags")
def clean_version(self): def clean_version(self):
version = self.cleaned_data['version'] version = self.cleaned_data["version"]
software_product = self.cleaned_data['software_product'] software_product = self.cleaned_data["software_product"]
if version not in software_product.softwareproductversion_set.all(): if version not in software_product.softwareproductversion_set.all():
raise forms.ValidationError(_(f"Version `{version}` doesn't exist on {software_product}, make sure you've " raise forms.ValidationError(
f"selected a compatible version or first select the software product.")) _(
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 return version
def clean(self):
if not any([self.cleaned_data['device'], self.cleaned_data['virtualmachine']]):
raise forms.ValidationError(_("Installation requires atleast one virtualmachine or device destination."))
return super(SoftwareProductInstallationForm, self).clean()
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm): class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductInstallation model = SoftwareProductInstallation
fieldsets = ( fieldsets = (
(None, ('q', 'tag',)), (
None,
(
"q",
"tag",
),
),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -72,15 +69,15 @@ class SoftwareProductInstallationImportForm(NetBoxModelImportForm):
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm): class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
queryset=SoftwareProduct.objects.all(), version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
required=False
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
model = SoftwareProductInstallation model = SoftwareProductInstallation
fieldsets = ( fieldsets = (
(None, ('software_product', 'version',)), (
None,
(
"software_product",
"version",
),
),
) )
+5 -7
View File
@@ -4,18 +4,18 @@ from django.utils.translation import gettext as _
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.widgets import APISelect
class SoftwareProductVersionForm(NetBoxModelForm): class SoftwareProductVersionForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductVersion object.""" """Form for creating a new SoftwareProductVersion object."""
name = forms.CharField(label=_("Version")) name = forms.CharField(label=_("Version"))
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
widget=APISelect( widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
) )
class Meta: class Meta:
@@ -25,9 +25,7 @@ class SoftwareProductVersionForm(NetBoxModelForm):
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm): class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductVersion model = SoftwareProductVersion
fieldsets = ( fieldsets = ((None, ("q", "tag")),)
(None, ('q', 'tag')),
)
tag = TagFilterField(model) tag = TagFilterField(model)
@@ -0,0 +1,19 @@
# Generated by Django 4.2.5 on 2023-09-22 00:34
from django.db import migrations
import netbox_slm.models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0005_add_software_license'),
]
operations = [
migrations.AddField(
model_name='softwarelicense',
name='stored_location_url',
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.2.5 on 2023-10-04 10:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0036_virtualmachine_config_template'),
('netbox_slm', '0006_softwarelicense_stored_location_url'),
]
operations = [
migrations.AddField(
model_name='softwareproductinstallation',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'),
),
migrations.AlterField(
model_name='softwarelicense',
name='installation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'),
),
migrations.AddConstraint(
model_name='softwareproductinstallation',
constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'),
),
]
+66 -48
View File
@@ -4,17 +4,23 @@ from django.utils import safestring
from netbox.models import NetBoxModel from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.validators import EnhancedURLValidator
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): class SoftwareProduct(NetBoxModel):
name = models.CharField(max_length=128) name = models.CharField(max_length=128)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
to='dcim.Manufacturer',
on_delete=models.PROTECT,
null=True, blank=True
)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -26,17 +32,23 @@ class SoftwareProduct(NetBoxModel):
def get_installation_count(self): def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count() count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format( return (
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}", safestring.mark_safe(
count=count '<a href="{url}">{count}</a>'.format(
)) if count else "0" url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
count=count,
)
)
if count
else "0"
)
class SoftwareProductVersion(NetBoxModel): class SoftwareProductVersion(NetBoxModel):
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
software_product = models.ForeignKey( software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct', to="netbox_slm.SoftwareProduct",
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@@ -50,48 +62,58 @@ class SoftwareProductVersion(NetBoxModel):
def get_installation_count(self): def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count() count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format( return (
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}", safestring.mark_safe(
count=count '<a href="{url}">{count}</a>'.format(
)) if count else "0" url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}",
count=count,
)
)
if count
else "0"
)
class SoftwareProductInstallation(NetBoxModel): class SoftwareProductInstallation(NetBoxModel):
device = models.ForeignKey( device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
to='dcim.Device',
on_delete=models.PROTECT,
null=True,
blank=True
)
virtualmachine = models.ForeignKey( virtualmachine = models.ForeignKey(
to='virtualization.VirtualMachine', to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
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,
) )
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
def __str__(self): def __str__(self):
return f"{self.pk} ({self.platform})" return f"{self.pk} ({self.platform})"
class Meta:
constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_platform",
check=(
models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
),
violation_error_message="Installation requires exactly one platform destination.",
)
]
def get_absolute_url(self): def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk}) return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
@property @property
def platform(self): def platform(self):
return self.device or self.virtualmachine return self.device or self.virtualmachine or self.cluster
def render_type(self): def render_type(self):
return 'device' if self.device else 'virtualmachine' if self.device:
return "device"
if self.virtualmachine:
return "virtualmachine"
return "cluster"
class SoftwareLicense(NetBoxModel): class SoftwareLicense(NetBoxModel):
@@ -100,24 +122,14 @@ class SoftwareLicense(NetBoxModel):
type = models.CharField(max_length=128) type = models.CharField(max_length=128)
stored_location = models.CharField(max_length=255, null=True, blank=True) 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) start_date = models.DateField(null=True, blank=True)
expiration_date = models.DateField(null=True, blank=True) expiration_date = models.DateField(null=True, blank=True)
software_product = models.ForeignKey( software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
to='netbox_slm.SoftwareProduct', version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
on_delete=models.PROTECT,
)
version = models.ForeignKey(
to='netbox_slm.SoftwareProductVersion',
on_delete=models.PROTECT,
null=True,
blank=True
)
installation = models.ForeignKey( installation = models.ForeignKey(
to='netbox_slm.SoftwareProductInstallation', to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
on_delete=models.PROTECT,
null=True,
blank=True
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -127,3 +139,9 @@ class SoftwareLicense(NetBoxModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk}) return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
@property
def stored_location_txt(self):
if self.stored_location_url and not self.stored_location:
return "Link"
return self.stored_location
+12 -12
View File
@@ -3,8 +3,8 @@ from utilities.choices import ButtonColorChoices
menu_items = ( menu_items = (
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_slm:softwareproduct_list', link="plugins:netbox_slm:softwareproduct_list",
link_text='Software Products', link_text="Software Products",
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add", "plugins:netbox_slm:softwareproduct_add",
@@ -20,11 +20,11 @@ menu_items = (
ButtonColorChoices.CYAN, ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproduct"], permissions=["netbox_slm.add_softwareproduct"],
), ),
) ),
), ),
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_slm:softwareproductversion_list', link="plugins:netbox_slm:softwareproductversion_list",
link_text='Versions', link_text="Versions",
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add", "plugins:netbox_slm:softwareproductversion_add",
@@ -40,11 +40,11 @@ menu_items = (
ButtonColorChoices.CYAN, ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductversion"], permissions=["netbox_slm.add_softwareproductversion"],
), ),
) ),
), ),
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_slm:softwareproductinstallation_list', link="plugins:netbox_slm:softwareproductinstallation_list",
link_text='Installations', link_text="Installations",
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add", "plugins:netbox_slm:softwareproductinstallation_add",
@@ -60,11 +60,11 @@ menu_items = (
ButtonColorChoices.CYAN, ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductinstallation"], permissions=["netbox_slm.add_softwareproductinstallation"],
), ),
) ),
), ),
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_slm:softwarelicense_list', link="plugins:netbox_slm:softwarelicense_list",
link_text='Licenses', link_text="Licenses",
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add", "plugins:netbox_slm:softwarelicense_add",
@@ -80,6 +80,6 @@ menu_items = (
ButtonColorChoices.CYAN, ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwarelicense"], permissions=["netbox_slm.add_softwarelicense"],
), ),
) ),
), ),
) )
+41 -73
View File
@@ -1,6 +1,5 @@
import django_tables2 as tables import django_tables2 as tables
from django.db.models import Count from django.db.models import Count, F, Value
from django_tables2.utils import Accessor
from netbox.tables import NetBoxTable, ToggleColumn, columns from netbox.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -11,15 +10,10 @@ class SoftwareProductTable(NetBoxTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
manufacturer = tables.Column( manufacturer = tables.Column(accessor="manufacturer", linkify=True)
accessor=Accessor('manufacturer'), installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
tags = columns.TagColumn( tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
url_name="plugins:netbox_slm:softwareproduct_list",
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareProduct model = SoftwareProduct
@@ -47,9 +41,9 @@ class SoftwareProductTable(NetBoxTable):
) )
def order_installations(self, queryset, is_descending): def order_installations(self, queryset, is_descending):
queryset = queryset.annotate( queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
count=Count('softwareproductinstallation__id') ("-" if is_descending else "") + "count"
).order_by(("-" if is_descending else "") + "count") )
return queryset, True return queryset, True
@@ -57,20 +51,12 @@ class SoftwareProductVersionTable(NetBoxTable):
"""Table for displaying SoftwareProductVersion objects.""" """Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Version') name = tables.LinkColumn(verbose_name="Version")
software_product = tables.Column( software_product = tables.Column(accessor="software_product", linkify=True)
accessor=Accessor('software_product'), manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
linkify=True installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
)
manufacturer = tables.Column(
accessor=Accessor('software_product__manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
tags = columns.TagColumn( tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
url_name="plugins:netbox_slm:softwareproductversion_list",
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareProductVersion model = SoftwareProductVersion
@@ -98,9 +84,9 @@ class SoftwareProductVersionTable(NetBoxTable):
) )
def order_installations(self, queryset, is_descending): def order_installations(self, queryset, is_descending):
queryset = queryset.annotate( queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
count=Count('softwareproductinstallation__id') ("-" if is_descending else "") + "count"
).order_by(("-" if is_descending else "") + "count") )
return queryset, True return queryset, True
@@ -109,31 +95,16 @@ class SoftwareProductInstallationTable(NetBoxTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
device = tables.Column(
accessor=Accessor('device'),
linkify=True
)
virtualmachine = tables.Column(
accessor=Accessor('virtualmachine'),
linkify=True
)
platform = tables.Column(
accessor=Accessor('platform'),
linkify=True
)
type = tables.Column(accessor='render_type')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
version = tables.Column(
accessor=Accessor('version'),
linkify=True
)
tags = columns.TagColumn( device = tables.Column(accessor="device", linkify=True)
url_name="plugins:netbox_slm:softwareproductinstallation_list", virtualmachine = tables.Column(accessor="virtualmachine", linkify=True)
) cluster = tables.Column(accessor="cluster", linkify=True)
platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type")
software_product = tables.Column(accessor="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): class Meta(NetBoxTable.Meta):
model = SoftwareProductInstallation model = SoftwareProductInstallation
@@ -156,12 +127,18 @@ class SoftwareProductInstallationTable(NetBoxTable):
) )
def order_platform(self, queryset, is_descending): def order_platform(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine")) device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name"))
return queryset, True 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): def order_type(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine")) device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device"))
return queryset, True 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
def render_software_product(self, value, **kwargs): def render_software_product(self, value, **kwargs):
return f"{kwargs['record'].software_product.manufacturer.name} - {value}" return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
@@ -173,23 +150,14 @@ class SoftwareLicenseTable(NetBoxTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
type = tables.Column(accessor='render_type') type = tables.Column()
software_product = tables.Column( stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
accessor=Accessor('software_product'),
linkify=True
)
version = tables.Column(
accessor=Accessor('version'),
linkify=True
)
installation = tables.Column(
accessor=Accessor('installation'),
linkify=True
)
tags = columns.TagColumn( software_product = tables.Column(accessor="software_product", linkify=True)
url_name="plugins:netbox_slm:softwarelicense_list", version = tables.Column(accessor="version", linkify=True)
) installation = tables.Column(accessor="installation", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareLicense model = SoftwareLicense
@@ -27,7 +27,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Stored location</th> <th scope="row">Stored location</th>
<td>{{ object.stored_location }}</td> {% 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>
<tr> <tr>
<th scope="row">Start date</th> <th scope="row">Start date</th>
@@ -18,11 +18,16 @@
<th scope="row">Device</th> <th scope="row">Device</th>
<td>{{ object.device }}</td> <td>{{ object.device }}</td>
</tr> </tr>
{% else %} {% elif object.virtualmachine %}
<tr> <tr>
<th scope="row">Virtualmachine</th> <th scope="row">Virtualmachine</th>
<td>{{ object.virtualmachine }}</td> <td>{{ object.virtualmachine }}</td>
</tr> </tr>
{% else %}
<tr>
<th scope="row">Cluster</th>
<td>{{ object.cluster }}</td>
</tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">Software Product</th> <th scope="row">Software Product</th>
+38 -4
View File
@@ -1,5 +1,7 @@
from django.test import TestCase from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from virtualization.models import Cluster, ClusterType, VirtualMachine
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -9,16 +11,29 @@ class ModelTestCase(TestCase):
self.v_name = "test version" self.v_name = "test version"
self.l_name = "test license" self.l_name = "test license"
manufacturer = Manufacturer.objects.create(name="test manufacturer")
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
device_role = DeviceRole.objects.create(name="test device role")
site = Site.objects.create(name="test site")
self.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
self.vm = VirtualMachine.objects.create(name="test VM")
cluster_type = ClusterType.objects.create(name="test cluster type")
self.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
self.test_url = "https://github.com/ICTU/netbox_slm"
self.software_product = SoftwareProduct.objects.create(name=self.p_name) self.software_product = SoftwareProduct.objects.create(name=self.p_name)
self.software_product_version = SoftwareProductVersion.objects.create( self.software_product_version = SoftwareProductVersion.objects.create(
name=self.v_name, software_product=self.software_product name=self.v_name, software_product=self.software_product
) )
self.software_product_installation = SoftwareProductInstallation.objects.create( self.software_product_installation = SoftwareProductInstallation.objects.create(
software_product=self.software_product, version=self.software_product_version virtualmachine=self.vm, software_product=self.software_product, version=self.software_product_version
) )
self.software_license = SoftwareLicense.objects.create( self.software_license = SoftwareLicense.objects.create(
name=self.l_name, software_product=self.software_product, name=self.l_name,
version=self.software_product_version, installation=self.software_product_installation software_product=self.software_product,
version=self.software_product_version,
installation=self.software_product_installation,
stored_location_url=self.test_url,
) )
def test_model_name(self): def test_model_name(self):
@@ -40,4 +55,23 @@ class ModelTestCase(TestCase):
def test_product_installation_methods(self): def test_product_installation_methods(self):
self.assertEqual("virtualmachine", self.software_product_installation.render_type()) self.assertEqual("virtualmachine", self.software_product_installation.render_type())
self.assertIsNone(self.software_product_installation.platform) 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)
+78 -67
View File
@@ -6,94 +6,105 @@ from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareP
urlpatterns = [ urlpatterns = [
# SoftwareProduct # SoftwareProduct
path("software-products/", views.SoftwareProductListView.as_view(), path("software-products/", views.SoftwareProductListView.as_view(), name="softwareproduct_list"),
name='softwareproduct_list'), path("software-products/add/", views.SoftwareProductEditView.as_view(), name="softwareproduct_add"),
path("software-products/add/", views.SoftwareProductEditView.as_view(), path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
name='softwareproduct_add'), path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), path(
name="softwareproduct_import"), "software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), ),
name="softwareproduct_bulk_edit"), path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), path(
name="softwareproduct_bulk_delete"), "software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), ),
name="softwareproduct"), path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
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( path(
"software-products/<int:pk>/changelog/", "software-products/<int:pk>/changelog/",
ObjectChangeLogView.as_view(), ObjectChangeLogView.as_view(),
name="softwareproduct_changelog", name="softwareproduct_changelog",
kwargs={"model": SoftwareProduct}, kwargs={"model": SoftwareProduct},
), ),
# SoftwareProductVersion # SoftwareProductVersion
path("versions/", views.SoftwareProductVersionListView.as_view(), path("versions/", views.SoftwareProductVersionListView.as_view(), name="softwareproductversion_list"),
name='softwareproductversion_list'), path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_add"),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), path(
name='softwareproductversion_add'), "versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), ),
name="softwareproductversion_import"), path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), path(
name="softwareproductversion_bulk_edit"), "versions/delete/",
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), views.SoftwareProductVersionBulkDeleteView.as_view(),
name="softwareproductversion_bulk_delete"), name="softwareproductversion_bulk_delete",
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), ),
name="softwareproductversion"), path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), path(
name="softwareproductversion_delete"), "versions/<int:pk>/delete/",
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), views.SoftwareProductVersionDeleteView.as_view(),
name="softwareproductversion_edit"), name="softwareproductversion_delete",
),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path( path(
"versions/<int:pk>/changelog/", "versions/<int:pk>/changelog/",
ObjectChangeLogView.as_view(), ObjectChangeLogView.as_view(),
name="softwareproductversion_changelog", name="softwareproductversion_changelog",
kwargs={"model": SoftwareProductVersion}, kwargs={"model": SoftwareProductVersion},
), ),
# SoftwareProductInstallation # SoftwareProductInstallation
path("installations/", views.SoftwareProductInstallationListView.as_view(), path(
name='softwareproductinstallation_list'), "installations/",
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), views.SoftwareProductInstallationListView.as_view(),
name='softwareproductinstallation_add'), name="softwareproductinstallation_list",
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), ),
name="softwareproductinstallation_import"), path(
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), "installations/add/",
name="softwareproductinstallation_bulk_edit"), views.SoftwareProductInstallationEditView.as_view(),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_add",
name="softwareproductinstallation_bulk_delete"), ),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(), path(
name="softwareproductinstallation"), "installations/import/",
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(), views.SoftwareProductInstallationBulkImportView.as_view(),
name="softwareproductinstallation_delete"), name="softwareproductinstallation_import",
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(), ),
name="softwareproductinstallation_edit"), 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( path(
"installations/<int:pk>/changelog/", "installations/<int:pk>/changelog/",
ObjectChangeLogView.as_view(), ObjectChangeLogView.as_view(),
name="softwareproductinstallation_changelog", name="softwareproductinstallation_changelog",
kwargs={"model": SoftwareProductInstallation}, kwargs={"model": SoftwareProductInstallation},
), ),
# SoftwareLicense # SoftwareLicense
path("licenses/", views.SoftwareLicenseListView.as_view(), path("licenses/", views.SoftwareLicenseListView.as_view(), name="softwarelicense_list"),
name='softwarelicense_list'), path("licenses/add/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_add"),
path("licenses/add/", views.SoftwareLicenseEditView.as_view(), path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(), name="softwarelicense_import"),
name='softwarelicense_add'), path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(), name="softwarelicense_bulk_edit"),
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(), path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(), name="softwarelicense_bulk_delete"),
name="softwarelicense_import"), path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(), name="softwarelicense"),
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(), path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(), name="softwarelicense_delete"),
name="softwarelicense_bulk_edit"), path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_edit"),
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(),
name="softwarelicense_bulk_delete"),
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(),
name="softwarelicense"),
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(),
name="softwarelicense_delete"),
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(),
name="softwarelicense_edit"),
path( path(
"licenses/<int:pk>/changelog/", "licenses/<int:pk>/changelog/",
ObjectChangeLogView.as_view(), ObjectChangeLogView.as_view(),
+36 -4
View File
@@ -1,4 +1,36 @@
from .software_license import * from .software_license import (
from .software_product import * SoftwareLicenseListView,
from .software_product_installation import * SoftwareLicenseView,
from .software_product_version import * SoftwareLicenseEditView,
SoftwareLicenseDeleteView,
SoftwareLicenseBulkImportView,
SoftwareLicenseBulkEditView,
SoftwareLicenseBulkDeleteView,
)
from .software_product import (
SoftwareProductListView,
SoftwareProductView,
SoftwareProductEditView,
SoftwareProductDeleteView,
SoftwareProductBulkImportView,
SoftwareProductBulkEditView,
SoftwareProductBulkDeleteView,
)
from .software_product_installation import (
SoftwareProductInstallationListView,
SoftwareProductInstallationView,
SoftwareProductInstallationEditView,
SoftwareProductInstallationDeleteView,
SoftwareProductInstallationBulkImportView,
SoftwareProductInstallationBulkEditView,
SoftwareProductInstallationBulkDeleteView,
)
from .software_product_version import (
SoftwareProductVersionListView,
SoftwareProductVersionView,
SoftwareProductVersionEditView,
SoftwareProductVersionDeleteView,
SoftwareProductVersionBulkImportView,
SoftwareProductVersionBulkEditView,
SoftwareProductVersionBulkDeleteView,
)
+73
View File
@@ -0,0 +1,73 @@
[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.9"
license = { text = "Apache-2.0" }
authors = [
{name = "ICTU", email = "open-source-projects@ictu.nl"},
]
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.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
dependencies = []
[project.optional-dependencies]
build = [
"build == 1.0.3",
"setuptools == 68.2.0",
"twine == 3.7.1",
]
tools = [
"black == 23.3.0",
"ruff == 0.0.275",
]
[tool.setuptools.dynamic]
version = {attr = "netbox_slm.__version__"}
[tool.setuptools.packages.find]
include = ["netbox_slm"]
[tool.black]
line-length = 120
target-version = ["py39"]
color = true
check = true
diff = true
fast = true
extend-exclude = '''
netbox_slm/migrations/.*.py
'''
[tool.ruff]
line-length = 120
target-version = "py39"
format = "github"
src = ["netbox_slm"]
[tool.ruff.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
]
-26
View File
@@ -1,26 +0,0 @@
[metadata]
name = netbox-slm
version = 1.4
description = Software Lifecycle Management Netbox Plugin.
author = ICTU
author_email = open-source-projects@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.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options]
include_package_data = true
packages = find:
python_requires = >=3.9
-3
View File
@@ -1,3 +0,0 @@
from setuptools import setup
setup()