diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index 05bb650..1a3bb72 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -1,6 +1,6 @@ from extras.plugins import PluginConfig -__version__ = "1.4" +__version__ = "1.5" class SLMConfig(PluginConfig): diff --git a/netbox_slm/api/serializers.py b/netbox_slm/api/serializers.py index 5b3f61c..da53a06 100644 --- a/netbox_slm/api/serializers.py +++ b/netbox_slm/api/serializers.py @@ -61,6 +61,7 @@ class Meta: "url", "device", "virtualmachine", + "cluster", "software_product", "version", "tags", diff --git a/netbox_slm/filtersets.py b/netbox_slm/filtersets.py index 72741e5..aee4a03 100644 --- a/netbox_slm/filtersets.py +++ b/netbox_slm/filtersets.py @@ -74,5 +74,6 @@ def search(self, queryset, name, 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) diff --git a/netbox_slm/forms/software_product_installation.py b/netbox_slm/forms/software_product_installation.py index dc107f3..5e32d0a 100644 --- a/netbox_slm/forms/software_product_installation.py +++ b/netbox_slm/forms/software_product_installation.py @@ -7,7 +7,7 @@ from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion from utilities.forms.fields import DynamicModelChoiceField, TagFilterField from utilities.forms.widgets import APISelect -from virtualization.models import VirtualMachine +from virtualization.models import VirtualMachine, Cluster class SoftwareProductInstallationForm(NetBoxModelForm): @@ -15,6 +15,7 @@ class SoftwareProductInstallationForm(NetBoxModelForm): 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, @@ -31,7 +32,7 @@ class SoftwareProductInstallationForm(NetBoxModelForm): 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"] @@ -45,11 +46,6 @@ def clean_version(self): ) 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 diff --git a/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py b/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py new file mode 100644 index 0000000..ea5d78b --- /dev/null +++ b/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py @@ -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.'), + ), + ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index d9f3107..47a0abd 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -79,6 +79,7 @@ class SoftwareProductInstallation(NetBoxModel): virtualmachine = models.ForeignKey( to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True ) + cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True) software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT) @@ -87,15 +88,32 @@ class SoftwareProductInstallation(NetBoxModel): 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): @@ -111,7 +129,7 @@ class SoftwareLicense(NetBoxModel): 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() diff --git a/netbox_slm/tables.py b/netbox_slm/tables.py index 3e3ab9b..e49afdc 100644 --- a/netbox_slm/tables.py +++ b/netbox_slm/tables.py @@ -1,5 +1,5 @@ import django_tables2 as tables -from django.db.models import Count +from django.db.models import Count, F, Value from netbox.tables import NetBoxTable, ToggleColumn, columns from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense @@ -95,8 +95,10 @@ class SoftwareProductInstallationTable(NetBoxTable): pk = ToggleColumn() name = tables.LinkColumn() + 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) @@ -125,12 +127,18 @@ class Meta(NetBoxTable.Meta): ) 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}" diff --git a/netbox_slm/templates/netbox_slm/softwareproductinstallation.html b/netbox_slm/templates/netbox_slm/softwareproductinstallation.html index 9965e33..ac652a5 100644 --- a/netbox_slm/templates/netbox_slm/softwareproductinstallation.html +++ b/netbox_slm/templates/netbox_slm/softwareproductinstallation.html @@ -18,11 +18,16 @@
Device {{ object.device }} -{% else %} +{% elif object.virtualmachine %} Virtualmachine {{ object.virtualmachine }} +{% else %} + + Cluster + {{ object.cluster }} + {% endif %} Software Product diff --git a/netbox_slm/tests/test_models.py b/netbox_slm/tests/test_models.py index 9d156ff..c033ac0 100644 --- a/netbox_slm/tests/test_models.py +++ b/netbox_slm/tests/test_models.py @@ -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,18 +11,29 @@ def setUp(self): 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, + stored_location_url=self.test_url, ) def test_model_name(self): @@ -42,4 +55,23 @@ def test_get_installation_count(self): 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)