Add Installation foreign key to VM Cluster (#29)

Closes #17
This commit is contained in:
wkoot
2023-10-04 18:23:08 +02:00
committed by GitHub
parent 2501e41b7c
commit d082af4a9b
9 changed files with 109 additions and 19 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
from extras.plugins import PluginConfig from extras.plugins import PluginConfig
__version__ = "1.4" __version__ = "1.5"
class SLMConfig(PluginConfig): class SLMConfig(PluginConfig):
+1
View File
@@ -61,6 +61,7 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
"url", "url",
"device", "device",
"virtualmachine", "virtualmachine",
"cluster",
"software_product", "software_product",
"version", "version",
"tags", "tags",
+1
View File
@@ -74,5 +74,6 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
| Q(version__name__icontains=value) | Q(version__name__icontains=value)
| Q(installation__device__name__icontains=value) | Q(installation__device__name__icontains=value)
| Q(installation__virtualmachine__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)
@@ -7,7 +7,7 @@ from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulk
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.widgets import APISelect from utilities.forms.widgets import APISelect
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine, Cluster
class SoftwareProductInstallationForm(NetBoxModelForm): class SoftwareProductInstallationForm(NetBoxModelForm):
@@ -15,6 +15,7 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False) device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False) virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
@@ -31,7 +32,7 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
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"]
@@ -45,11 +46,6 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
) )
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
@@ -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.'),
),
]
+21 -3
View File
@@ -79,6 +79,7 @@ class SoftwareProductInstallation(NetBoxModel):
virtualmachine = models.ForeignKey( virtualmachine = models.ForeignKey(
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True 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) software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", 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): 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):
@@ -111,7 +129,7 @@ class SoftwareLicense(NetBoxModel):
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) 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) 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", 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() objects = RestrictedQuerySet.as_manager()
+13 -5
View File
@@ -1,5 +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 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
@@ -95,8 +95,10 @@ class SoftwareProductInstallationTable(NetBoxTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
device = tables.Column(accessor="device", linkify=True) device = tables.Column(accessor="device", linkify=True)
virtualmachine = tables.Column(accessor="virtualmachine", linkify=True) virtualmachine = tables.Column(accessor="virtualmachine", linkify=True)
cluster = tables.Column(accessor="cluster", linkify=True)
platform = tables.Column(accessor="platform", linkify=True) platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type") type = tables.Column(accessor="render_type")
software_product = tables.Column(accessor="software_product", linkify=True) software_product = tables.Column(accessor="software_product", linkify=True)
@@ -125,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}"
@@ -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>
+34 -2
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,18 +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, name=self.l_name,
software_product=self.software_product, software_product=self.software_product,
version=self.software_product_version, version=self.software_product_version,
installation=self.software_product_installation, installation=self.software_product_installation,
stored_location_url=self.test_url,
) )
def test_model_name(self): def test_model_name(self):
@@ -42,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)