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
out/production
*.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:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.2`` as
``configuration/plugins.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
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;
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
```
# make sure to update the version in netbox_slm/__init__.py
$ python setup.py sdist
$ python -m build
$ 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)
+3 -2
View File
@@ -7,10 +7,11 @@ ENV PATH="/opt/netbox/venv/bin:$PATH"
COPY ../ /build
RUN python setup.py sdist
RUN pip install -U .[build]
RUN python -m build
RUN pip install --no-index /build
FROM netboxcommunity/netbox:v3.4.7
FROM netboxcommunity/netbox:v3.6.1
ARG PYTHON_VERSION=3.10
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
+1 -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
COPY ../ci/requirements_ci.txt /ci/
+1 -1
View File
@@ -1,6 +1,6 @@
PLUGINS = ['netbox_slm']
DEBUG = True
SECRET_KEY = 'dummy'
SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
DEVELOPER = True
PLUGINS_CONFIG = {
'netbox_slm': dict(
+1 -1
View File
@@ -1,7 +1,7 @@
version: '3.7'
services:
netbox: &netbox
image: netboxcommunity/netbox:${VERSION-v3.3-2.2.0}
image: netboxcommunity/netbox:${VERSION-v3.6.1}
depends_on:
- postgres
- redis
+10 -10
View File
@@ -1,18 +1,18 @@
from extras.plugins import PluginConfig
__version__ = "1.5"
class SLMConfig(PluginConfig):
name = 'netbox_slm'
verbose_name = 'Software Lifecycle Management'
description = 'Software Lifecycle Management Netbox Plugin.'
version = '1.4'
author = 'ICTU'
author_email = 'open-source-projects@ictu.nl'
base_url = 'slm'
name = "netbox_slm"
verbose_name = "Software Lifecycle Management"
description = "Software Lifecycle Management Netbox Plugin."
version = __version__
author = "ICTU"
author_email = "open-source-projects@ictu.nl"
base_url = "slm"
required_settings = []
default_settings = {
'version_info': False
}
default_settings = {"version_info": False}
config = SLMConfig
+48 -18
View File
@@ -6,15 +6,19 @@ from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareP
class SoftwareProductSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
)
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',
"id",
"display",
"url",
"name",
"tags",
"custom_field_data",
"created",
"last_updated",
]
def get_display(self, obj):
@@ -23,15 +27,20 @@ class SoftwareProductSerializer(NetBoxModelSerializer):
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
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',
"id",
"display",
"url",
"name",
"software_product",
"tags",
"custom_field_data",
"created",
"last_updated",
]
def get_display(self, obj):
@@ -47,8 +56,18 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
class Meta:
model = SoftwareProductInstallation
fields = [
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version',
'tags', 'custom_field_data', 'created', 'last_updated',
"id",
"display",
"url",
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
"custom_field_data",
"created",
"last_updated",
]
def get_display(self, obj):
@@ -57,16 +76,27 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
class SoftwareLicenseSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwarelicense-detail"
)
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail")
class Meta:
model = SoftwareLicense
fields = [
'id', 'display', 'url', 'name', 'description', 'type', 'stored_location',
'start_date', 'expiration_date', 'software_product', 'version', 'installation',
'tags', 'custom_field_data', 'created', 'last_updated',
"id",
"display",
"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):
+20 -17
View File
@@ -24,17 +24,17 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
class Meta:
model = SoftwareProductVersion
fields = (
"software_product",
)
fields = ("software_product",)
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | \
Q(software_product__name__icontains=value) | \
Q(software_product__manufacturer__name__icontains=value)
qs_filter = (
Q(name__icontains=value)
| Q(software_product__name__icontains=value)
| Q(software_product__manufacturer__name__icontains=value)
)
return queryset.filter(qs_filter)
@@ -43,17 +43,17 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
class Meta:
model = SoftwareProductInstallation
fields = (
"software_product",
)
fields = ("software_product",)
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)
qs_filter = (
Q(software_product__name__icontains=value)
| Q(software_product__manufacturer__name__icontains=value)
| Q(version__name__icontains=value)
)
return queryset.filter(qs_filter)
@@ -68,9 +68,12 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__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)
qs_filter = (
Q(name__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)
)
return queryset.filter(qs_filter)
+24 -4
View File
@@ -1,4 +1,24 @@
from .software_license import *
from .software_product import *
from .software_product_installation import *
from .software_product_version import *
from .software_license import (
SoftwareLicenseForm,
SoftwareLicenseFilterForm,
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.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
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):
"""Form for creating a new SoftwareLicense object."""
stored_location_url = LaxURLField(required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params={
'software_product': '$software_product',
}
"software_product": "$software_product",
},
)
installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
@@ -32,20 +33,39 @@ class SoftwareLicenseForm(NetBoxModelForm):
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
),
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:
model = SoftwareLicense
fields = ("name", "description", "type", "stored_location", "start_date", "expiration_date",
"software_product", "version", "installation", "tags")
fields = (
"name",
"description",
"type",
"stored_location",
"stored_location_url",
"start_date",
"expiration_date",
"software_product",
"version",
"installation",
"tags",
)
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
model = SoftwareLicense
fieldsets = (
(None, ('q', 'tag',)),
(
None,
(
"q",
"tag",
),
),
)
tag = TagFilterField(model)
@@ -54,23 +74,29 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
class SoftwareLicenseImportForm(NetBoxModelImportForm):
class Meta:
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):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
installation = DynamicModelChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
required=False
)
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
installation = DynamicModelChoiceField(queryset=SoftwareProductInstallation.objects.all(), required=False)
model = SoftwareLicense
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 netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProduct
from utilities.forms import DynamicModelChoiceField, TagFilterField
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
class SoftwareProductForm(NetBoxModelForm):
@@ -21,9 +21,7 @@ class SoftwareProductForm(NetBoxModelForm):
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct
fieldsets = (
(None, ('q', 'tag')),
)
fieldsets = ((None, ("q", "tag")),)
tag = TagFilterField(model)
@@ -31,7 +29,10 @@ class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
class SoftwareProductImportForm(NetBoxModelImportForm):
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer",)
fields = (
"name",
"manufacturer",
)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
@@ -5,61 +5,58 @@ from django.utils.translation import gettext as _
from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField
from virtualization.models import VirtualMachine
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.widgets import APISelect
from virtualization.models import VirtualMachine, Cluster
class SoftwareProductInstallationForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
)
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}),
query_params={
'software_product': '$software_product',
}
"software_product": "$software_product",
},
)
class Meta:
model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version", "tags")
fields = ("device", "virtualmachine", "cluster", "software_product", "version", "tags")
def clean_version(self):
version = self.cleaned_data['version']
software_product = self.cleaned_data['software_product']
version = self.cleaned_data["version"]
software_product = self.cleaned_data["software_product"]
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 "
f"selected a compatible version or first select the software product."))
raise forms.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
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):
model = SoftwareProductInstallation
fieldsets = (
(None, ('q', 'tag',)),
(
None,
(
"q",
"tag",
),
),
)
tag = TagFilterField(model)
@@ -72,15 +69,15 @@ class SoftwareProductInstallationImportForm(NetBoxModelImportForm):
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
software_product = DynamicModelChoiceField(queryset=SoftwareProduct.objects.all(), required=False)
version = DynamicModelChoiceField(queryset=SoftwareProductVersion.objects.all(), required=False)
model = SoftwareProductInstallation
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_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):
"""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")}
),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
)
class Meta:
@@ -25,9 +25,7 @@ class SoftwareProductVersionForm(NetBoxModelForm):
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductVersion
fieldsets = (
(None, ('q', 'tag')),
)
fieldsets = ((None, ("q", "tag")),)
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.'),
),
]
+64 -46
View File
@@ -4,17 +4,23 @@ from django.utils import safestring
from netbox.models import NetBoxModel
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):
name = models.CharField(max_length=128)
description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
null=True, blank=True
)
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
objects = RestrictedQuerySet.as_manager()
@@ -26,17 +32,23 @@ class SoftwareProduct(NetBoxModel):
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(
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"
count=count,
)
)
if count
else "0"
)
class SoftwareProductVersion(NetBoxModel):
name = models.CharField(max_length=64)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
to="netbox_slm.SoftwareProduct",
on_delete=models.PROTECT,
)
@@ -50,48 +62,58 @@ class SoftwareProductVersion(NetBoxModel):
def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
return safestring.mark_safe("<a href=\"{url}\">{count}</a>".format(
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"
count=count,
)
)
if count
else "0"
)
class SoftwareProductInstallation(NetBoxModel):
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.PROTECT,
null=True,
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
)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT,
)
version = models.ForeignKey(
to='netbox_slm.SoftwareProductVersion',
on_delete=models.PROTECT,
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",
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):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
@property
def platform(self):
return self.device or self.virtualmachine
return self.device or self.virtualmachine or self.cluster
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):
@@ -100,24 +122,14 @@ class SoftwareLicense(NetBoxModel):
type = models.CharField(max_length=128)
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)
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
)
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.PROTECT,
null=True,
blank=True
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
)
objects = RestrictedQuerySet.as_manager()
@@ -127,3 +139,9 @@ class SoftwareLicense(NetBoxModel):
def get_absolute_url(self):
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 = (
PluginMenuItem(
link='plugins:netbox_slm:softwareproduct_list',
link_text='Software Products',
link="plugins:netbox_slm:softwareproduct_list",
link_text="Software Products",
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add",
@@ -20,11 +20,11 @@ menu_items = (
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproduct"],
),
)
),
),
PluginMenuItem(
link='plugins:netbox_slm:softwareproductversion_list',
link_text='Versions',
link="plugins:netbox_slm:softwareproductversion_list",
link_text="Versions",
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add",
@@ -40,11 +40,11 @@ menu_items = (
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductversion"],
),
)
),
),
PluginMenuItem(
link='plugins:netbox_slm:softwareproductinstallation_list',
link_text='Installations',
link="plugins:netbox_slm:softwareproductinstallation_list",
link_text="Installations",
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add",
@@ -60,11 +60,11 @@ menu_items = (
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwareproductinstallation"],
),
)
),
),
PluginMenuItem(
link='plugins:netbox_slm:softwarelicense_list',
link_text='Licenses',
link="plugins:netbox_slm:softwarelicense_list",
link_text="Licenses",
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add",
@@ -80,6 +80,6 @@ menu_items = (
ButtonColorChoices.CYAN,
permissions=["netbox_slm.add_softwarelicense"],
),
)
),
),
)
+41 -73
View File
@@ -1,6 +1,5 @@
import django_tables2 as tables
from django.db.models import Count
from django_tables2.utils import Accessor
from django.db.models import Count, F, Value
from netbox.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -11,15 +10,10 @@ class SoftwareProductTable(NetBoxTable):
pk = ToggleColumn()
name = tables.LinkColumn()
manufacturer = tables.Column(
accessor=Accessor('manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
manufacturer = tables.Column(accessor="manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
tags = columns.TagColumn(
url_name="plugins:netbox_slm:softwareproduct_list",
)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProduct
@@ -47,9 +41,9 @@ class SoftwareProductTable(NetBoxTable):
)
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(
count=Count('softwareproductinstallation__id')
).order_by(("-" if is_descending else "") + "count")
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
@@ -57,20 +51,12 @@ class SoftwareProductVersionTable(NetBoxTable):
"""Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Version')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
manufacturer = tables.Column(
accessor=Accessor('software_product__manufacturer'),
linkify=True
)
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
name = tables.LinkColumn(verbose_name="Version")
software_product = tables.Column(accessor="software_product", linkify=True)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count", verbose_name="Installations")
tags = columns.TagColumn(
url_name="plugins:netbox_slm:softwareproductversion_list",
)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list")
class Meta(NetBoxTable.Meta):
model = SoftwareProductVersion
@@ -98,9 +84,9 @@ class SoftwareProductVersionTable(NetBoxTable):
)
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(
count=Count('softwareproductinstallation__id')
).order_by(("-" if is_descending else "") + "count")
queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by(
("-" if is_descending else "") + "count"
)
return queryset, True
@@ -109,31 +95,16 @@ class SoftwareProductInstallationTable(NetBoxTable):
pk = ToggleColumn()
name = tables.LinkColumn()
device = tables.Column(
accessor=Accessor('device'),
linkify=True
)
virtualmachine = tables.Column(
accessor=Accessor('virtualmachine'),
linkify=True
)
platform = tables.Column(
accessor=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(
url_name="plugins:netbox_slm:softwareproductinstallation_list",
)
device = tables.Column(accessor="device", linkify=True)
virtualmachine = tables.Column(accessor="virtualmachine", linkify=True)
cluster = tables.Column(accessor="cluster", linkify=True)
platform = tables.Column(accessor="platform", linkify=True)
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):
model = SoftwareProductInstallation
@@ -156,12 +127,18 @@ class SoftwareProductInstallationTable(NetBoxTable):
)
def order_platform(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
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):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
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
def render_software_product(self, value, **kwargs):
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
@@ -173,23 +150,14 @@ class SoftwareLicenseTable(NetBoxTable):
pk = ToggleColumn()
name = tables.LinkColumn()
type = tables.Column(accessor='render_type')
software_product = tables.Column(
accessor=Accessor('software_product'),
linkify=True
)
version = tables.Column(
accessor=Accessor('version'),
linkify=True
)
installation = tables.Column(
accessor=Accessor('installation'),
linkify=True
)
type = tables.Column()
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
tags = columns.TagColumn(
url_name="plugins:netbox_slm:softwarelicense_list",
)
software_product = tables.Column(accessor="software_product", linkify=True)
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):
model = SoftwareLicense
@@ -27,7 +27,11 @@
</tr>
<tr>
<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>
<th scope="row">Start date</th>
@@ -18,11 +18,16 @@
<th scope="row">Device</th>
<td>{{ object.device }}</td>
</tr>
{% else %}
{% elif object.virtualmachine %}
<tr>
<th scope="row">Virtualmachine</th>
<td>{{ object.virtualmachine }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Cluster</th>
<td>{{ object.cluster }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Software Product</th>
+38 -4
View File
@@ -1,5 +1,7 @@
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
@@ -9,16 +11,29 @@ class ModelTestCase(TestCase):
self.v_name = "test version"
self.l_name = "test license"
manufacturer = Manufacturer.objects.create(name="test manufacturer")
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
device_role = DeviceRole.objects.create(name="test device role")
site = Site.objects.create(name="test site")
self.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
self.vm = VirtualMachine.objects.create(name="test VM")
cluster_type = ClusterType.objects.create(name="test cluster type")
self.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
self.test_url = "https://github.com/ICTU/netbox_slm"
self.software_product = SoftwareProduct.objects.create(name=self.p_name)
self.software_product_version = SoftwareProductVersion.objects.create(
name=self.v_name, software_product=self.software_product
)
self.software_product_installation = SoftwareProductInstallation.objects.create(
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(
name=self.l_name, software_product=self.software_product,
version=self.software_product_version, installation=self.software_product_installation
name=self.l_name,
software_product=self.software_product,
version=self.software_product_version,
installation=self.software_product_installation,
stored_location_url=self.test_url,
)
def test_model_name(self):
@@ -40,4 +55,23 @@ class ModelTestCase(TestCase):
def test_product_installation_methods(self):
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 = [
# SoftwareProduct
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/", 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},
),
# SoftwareProductVersion
path("versions/", views.SoftwareProductVersionListView.as_view(),
name='softwareproductversion_list'),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(),
name='softwareproductversion_add'),
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(),
name="softwareproductversion_import"),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(),
name="softwareproductversion_bulk_edit"),
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(),
name="softwareproductversion_bulk_delete"),
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(),
name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(),
name="softwareproductversion_delete"),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(),
name="softwareproductversion_edit"),
path("versions/", views.SoftwareProductVersionListView.as_view(), name="softwareproductversion_list"),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_add"),
path(
"versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"
),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
path(
"versions/delete/",
views.SoftwareProductVersionBulkDeleteView.as_view(),
name="softwareproductversion_bulk_delete",
),
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path(
"versions/<int:pk>/delete/",
views.SoftwareProductVersionDeleteView.as_view(),
name="softwareproductversion_delete",
),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path(
"versions/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductversion_changelog",
kwargs={"model": SoftwareProductVersion},
),
# SoftwareProductInstallation
path("installations/", views.SoftwareProductInstallationListView.as_view(),
name='softwareproductinstallation_list'),
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(),
name='softwareproductinstallation_add'),
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(),
name="softwareproductinstallation_import"),
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(),
name="softwareproductinstallation_bulk_edit"),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(),
name="softwareproductinstallation_bulk_delete"),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(),
name="softwareproductinstallation"),
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(),
name="softwareproductinstallation_delete"),
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(),
name="softwareproductinstallation_edit"),
path(
"installations/",
views.SoftwareProductInstallationListView.as_view(),
name="softwareproductinstallation_list",
),
path(
"installations/add/",
views.SoftwareProductInstallationEditView.as_view(),
name="softwareproductinstallation_add",
),
path(
"installations/import/",
views.SoftwareProductInstallationBulkImportView.as_view(),
name="softwareproductinstallation_import",
),
path(
"installations/edit/",
views.SoftwareProductInstallationBulkEditView.as_view(),
name="softwareproductinstallation_bulk_edit",
),
path(
"installations/delete/",
views.SoftwareProductInstallationBulkDeleteView.as_view(),
name="softwareproductinstallation_bulk_delete",
),
path(
"installations/<int:pk>/",
views.SoftwareProductInstallationView.as_view(),
name="softwareproductinstallation",
),
path(
"installations/<int:pk>/delete/",
views.SoftwareProductInstallationDeleteView.as_view(),
name="softwareproductinstallation_delete",
),
path(
"installations/<int:pk>/edit/",
views.SoftwareProductInstallationEditView.as_view(),
name="softwareproductinstallation_edit",
),
path(
"installations/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductinstallation_changelog",
kwargs={"model": SoftwareProductInstallation},
),
# SoftwareLicense
path("licenses/", views.SoftwareLicenseListView.as_view(),
name='softwarelicense_list'),
path("licenses/add/", views.SoftwareLicenseEditView.as_view(),
name='softwarelicense_add'),
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(),
name="softwarelicense_import"),
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(),
name="softwarelicense_bulk_edit"),
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(),
name="softwarelicense_bulk_delete"),
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(),
name="softwarelicense"),
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(),
name="softwarelicense_delete"),
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(),
name="softwarelicense_edit"),
path("licenses/", views.SoftwareLicenseListView.as_view(), name="softwarelicense_list"),
path("licenses/add/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_add"),
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(), name="softwarelicense_import"),
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(), name="softwarelicense_bulk_edit"),
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(), name="softwarelicense_bulk_delete"),
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(), name="softwarelicense"),
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(), name="softwarelicense_delete"),
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(), name="softwarelicense_edit"),
path(
"licenses/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
+36 -4
View File
@@ -1,4 +1,36 @@
from .software_license import *
from .software_product import *
from .software_product_installation import *
from .software_product_version import *
from .software_license import (
SoftwareLicenseListView,
SoftwareLicenseView,
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()