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 @@