Skip to content

Commit

Permalink
Add Installation foreign key to VM Cluster (#29)
Browse files Browse the repository at this point in the history
Closes #17
  • Loading branch information
wkoot authored Oct 4, 2023
1 parent 2501e41 commit d082af4
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 19 deletions.
2 changes: 1 addition & 1 deletion netbox_slm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from extras.plugins import PluginConfig

__version__ = "1.4"
__version__ = "1.5"


class SLMConfig(PluginConfig):
Expand Down
1 change: 1 addition & 0 deletions netbox_slm/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Meta:
"url",
"device",
"virtualmachine",
"cluster",
"software_product",
"version",
"tags",
Expand Down
1 change: 1 addition & 0 deletions netbox_slm/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 3 additions & 7 deletions netbox_slm/forms/software_product_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
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):
"""Form for creating a new SoftwareProductInstallation object."""

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,
Expand All @@ -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"]
Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions netbox_slm/migrations/0007_softwareproductinstallation_cluster.py
Original file line number Diff line number Diff line change
@@ -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.'),
),
]
24 changes: 21 additions & 3 deletions netbox_slm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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):
Expand All @@ -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()
Expand Down
18 changes: 13 additions & 5 deletions netbox_slm/tables.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ <h5 class="card-header">
<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>
Expand Down
36 changes: 34 additions & 2 deletions netbox_slm/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand All @@ -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)

0 comments on commit d082af4

Please sign in to comment.