feat: add tenancy support for software licenses
Add tenant group and tenant assignments across the model, forms, filters, API, tables, imports, and tests. Replace the Docker-based setup with Git/Pip installation documentation for NetBox 4.6.5.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
@@ -27,6 +28,8 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"tags",
|
||||
"comments",
|
||||
"custom_field_data",
|
||||
@@ -38,6 +41,15 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant_group = attrs.get("tenant_group", getattr(self.instance, "tenant_group", None))
|
||||
tenant = attrs.get("tenant", getattr(self.instance, "tenant", None))
|
||||
if tenant_group and tenant and tenant.group_id != tenant_group.pk:
|
||||
raise serializers.ValidationError(
|
||||
{"tenant": "The selected tenant does not belong to the selected tenant group."}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
display = SerializerMethodField()
|
||||
|
||||
@@ -11,6 +11,7 @@ from netbox_slm.models import (
|
||||
SoftwareReleaseTypes,
|
||||
)
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
@@ -122,6 +123,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
|
||||
installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
|
||||
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label="Tenant Group")
|
||||
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label="Tenant")
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
@@ -139,6 +142,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
| Q(installation__device__name__icontains=value)
|
||||
| Q(installation__virtualmachine__name__icontains=value)
|
||||
| Q(installation__cluster__name__icontains=value)
|
||||
| Q(tenant_group__name__icontains=value)
|
||||
| Q(tenant__name__icontains=value)
|
||||
| Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -9,6 +9,7 @@ from netbox_slm.models import (
|
||||
SoftwareLicense,
|
||||
spdx_license_names,
|
||||
)
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
|
||||
from utilities.forms.fields import (
|
||||
CommentField,
|
||||
@@ -48,6 +49,17 @@ class SoftwareLicenseForm(NetBoxModelForm):
|
||||
),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
tenant_group = DynamicModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
label="Tenant Group",
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
query_params={"group_id": "$tenant_group"},
|
||||
label="Tenant",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
@@ -65,6 +77,8 @@ class SoftwareLicenseForm(NetBoxModelForm):
|
||||
"license_amount",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"tags",
|
||||
"comments",
|
||||
)
|
||||
@@ -84,6 +98,8 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
"software_product_id",
|
||||
"version_id",
|
||||
"installation_id",
|
||||
"tenant_group_id",
|
||||
"tenant_id",
|
||||
),
|
||||
)
|
||||
selector_fields = ("q", "filter_id", "name")
|
||||
@@ -112,6 +128,17 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label="Installation",
|
||||
)
|
||||
tenant_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
label="Tenant Group",
|
||||
)
|
||||
tenant_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
query_params={"group_id": "$tenant_group_id"},
|
||||
label="Tenant",
|
||||
)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
|
||||
@@ -132,6 +159,8 @@ class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
|
||||
"license_amount",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"tags",
|
||||
)
|
||||
|
||||
@@ -151,6 +180,8 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
),
|
||||
)
|
||||
nullable_fields = (
|
||||
@@ -164,6 +195,8 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
"license_amount",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
@@ -197,3 +230,14 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
),
|
||||
query_params=dict(software_product="$software_product"),
|
||||
)
|
||||
tenant_group = DynamicModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
label="Tenant Group",
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
query_params={"group_id": "$tenant_group"},
|
||||
label="Tenant",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("tenancy", "0001_initial"),
|
||||
("netbox_slm", "0010_softwarelicense_spdx_expression"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="softwarelicense",
|
||||
name="tenant_group",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="software_licenses",
|
||||
to="tenancy.tenantgroup",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="softwarelicense",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="software_licenses",
|
||||
to="tenancy.tenant",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -163,6 +163,20 @@ class SoftwareLicense(NetBoxModel):
|
||||
installation = models.ForeignKey(
|
||||
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
tenant_group = models.ForeignKey(
|
||||
to="tenancy.TenantGroup",
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="software_licenses",
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to="tenancy.Tenant",
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="software_licenses",
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -172,6 +186,11 @@ class SoftwareLicense(NetBoxModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.tenant_group and self.tenant and self.tenant.group_id != self.tenant_group_id:
|
||||
raise ValidationError({"tenant": "The selected tenant does not belong to the selected tenant group."})
|
||||
|
||||
@property
|
||||
def stored_location_txt(self):
|
||||
if self.stored_location_url and not self.stored_location:
|
||||
|
||||
@@ -144,6 +144,8 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
|
||||
version = tables.Column(accessor="version", linkify=True)
|
||||
installation = tables.Column(accessor="installation", linkify=True)
|
||||
tenant_group = tables.Column(accessor="tenant_group", verbose_name="Tenant Group", linkify=True)
|
||||
tenant = tables.Column(accessor="tenant", linkify=True)
|
||||
|
||||
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
|
||||
|
||||
@@ -162,6 +164,8 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"support",
|
||||
"license_amount",
|
||||
"tags",
|
||||
@@ -173,6 +177,8 @@ class SoftwareLicenseTable(NetBoxTable):
|
||||
"manufacturer",
|
||||
"software_product",
|
||||
"installation",
|
||||
"tenant_group",
|
||||
"tenant",
|
||||
"expiration_date",
|
||||
"tags",
|
||||
)
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object.installation|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant Group</th>
|
||||
<td>{{ object.tenant_group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant</th>
|
||||
<td>{{ object.tenant|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stored location</th>
|
||||
{% if object.stored_location_url %}
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
@@ -26,6 +27,8 @@ class SlmBaseTestCase(TestCase):
|
||||
cls.vm = VirtualMachine.objects.create(name="test VM")
|
||||
cluster_type = ClusterType.objects.create(name="test cluster type")
|
||||
cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
|
||||
cls.tenant_group = TenantGroup.objects.create(name="test tenant group", slug="test-tenant-group")
|
||||
cls.tenant = Tenant.objects.create(name="test tenant", slug="test-tenant", group=cls.tenant_group)
|
||||
|
||||
cls.software_product = SoftwareProduct.objects.create(name=cls.p_name)
|
||||
cls.software_product_version = SoftwareProductVersion.objects.create(
|
||||
@@ -40,6 +43,8 @@ class SlmBaseTestCase(TestCase):
|
||||
version=cls.software_product_version,
|
||||
installation=cls.software_product_installation,
|
||||
stored_location_url=cls.test_url,
|
||||
tenant_group=cls.tenant_group,
|
||||
tenant=cls.tenant,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -48,6 +53,8 @@ class SlmBaseTestCase(TestCase):
|
||||
SoftwareProductInstallation.objects.all().delete()
|
||||
SoftwareProductVersion.objects.all().delete()
|
||||
SoftwareProduct.objects.all().delete()
|
||||
Tenant.objects.all().delete()
|
||||
TenantGroup.objects.all().delete()
|
||||
Cluster.objects.all().delete()
|
||||
ClusterType.objects.all().delete()
|
||||
VirtualMachine.objects.all().delete()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
from .base import SlmBaseTestCase
|
||||
|
||||
|
||||
@@ -56,3 +59,15 @@ class ModelTestCase(SlmBaseTestCase):
|
||||
self.software_license.stored_location = "GitHub"
|
||||
self.software_license.save()
|
||||
self.assertEqual("GitHub", self.software_license.stored_location_txt)
|
||||
|
||||
def test_software_license_tenancy(self):
|
||||
self.assertEqual(self.tenant_group, self.software_license.tenant_group)
|
||||
self.assertEqual(self.tenant, self.software_license.tenant)
|
||||
|
||||
def test_software_license_rejects_tenant_from_other_group(self):
|
||||
other_group = TenantGroup.objects.create(name="other group", slug="other-group")
|
||||
other_tenant = Tenant.objects.create(name="other tenant", slug="other-tenant", group=other_group)
|
||||
self.software_license.tenant = other_tenant
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, "does not belong to the selected tenant group"):
|
||||
self.software_license.full_clean()
|
||||
|
||||
Reference in New Issue
Block a user