diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index f14ba1c86..873f18158 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -145,12 +145,6 @@ {% if object.disk %} {{ object.disk }} {% trans "GB" context "Abbreviation for gigabyte" %} - - - - {% elif object.disk_size %} - {{ object.disk_size }} {% trans "GB" context "Abbreviation for gigabyte" %} - ({{ object.virtualdisks.count }} {% trans "disks" %}) {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/virtualization/apps.py b/netbox/virtualization/apps.py index 8db943ea1..f0af9a163 100644 --- a/netbox/virtualization/apps.py +++ b/netbox/virtualization/apps.py @@ -5,7 +5,7 @@ class VirtualizationConfig(AppConfig): name = 'virtualization' def ready(self): - from . import search + from . import search, signals from .models import VirtualMachine from utilities.counters import connect_counters diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 6e108fedd..80794281b 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -240,6 +240,11 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): if self.instance.pk: + # Disable the disk field if one or more VirtualDisks have been created + if self.instance.virtualdisks.exists(): + self.fields['disk'].widget.attrs['disabled'] = True + self.fields['disk'].help_text = _("Disk size is managed via the attachment of virtual disks.") + # Compile list of choices for primary IPv4 and IPv6 addresses for family in [4, 6]: ip_choices = [(None, '---------')] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 970167bf3..634c59c9e 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Q +from django.db.models import Q, Sum from django.db.models.functions import Lower from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -120,12 +120,10 @@ class VirtualMachine(ContactsMixin, RenderConfigMixin, ConfigContextModel, Prima null=True, verbose_name=_('memory (MB)') ) - # TODO: Remove in v4.0 disk = models.PositiveIntegerField( blank=True, null=True, - verbose_name=_('disk (GB)'), - help_text=_('deprecated - use Virtual Disks') + verbose_name=_('disk (GB)') ) # Counter fields @@ -199,6 +197,17 @@ class VirtualMachine(ContactsMixin, RenderConfigMixin, ConfigContextModel, Prima ).format(device=self.device, cluster=self.cluster) }) + # Validate aggregate disk size + if self.pk: + total_disk = self.virtualdisks.aggregate(Sum('size', default=0))['size__sum'] + if total_disk and self.disk != total_disk: + raise ValidationError({ + 'disk': _( + "The specified disk size ({size}) must match the aggregate size of assigned virtual disks " + "({total_size})." + ).format(size=self.disk, total_size=total_disk) + }) + # Validate primary IP addresses interfaces = self.interfaces.all() if self.pk else None for family in (4, 6): diff --git a/netbox/virtualization/signals.py b/netbox/virtualization/signals.py new file mode 100644 index 000000000..2ee015211 --- /dev/null +++ b/netbox/virtualization/signals.py @@ -0,0 +1,16 @@ +from django.db.models import Sum +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver + +from .models import VirtualDisk, VirtualMachine + + +@receiver((post_delete, post_save), sender=VirtualDisk) +def update_circuit(instance, **kwargs): + """ + When a VirtualDisk has been modified, update the aggregate disk_size value of its VM. + """ + vm = instance.virtual_machine + VirtualMachine.objects.filter(pk=vm.pk).update( + disk=vm.virtualdisks.aggregate(Sum('size'))['size__sum'] + ) diff --git a/netbox/virtualization/tests/test_models.py b/netbox/virtualization/tests/test_models.py index 782b9f07f..c94ff930e 100644 --- a/netbox/virtualization/tests/test_models.py +++ b/netbox/virtualization/tests/test_models.py @@ -90,3 +90,28 @@ class VirtualMachineTestCase(TestCase): # Uniqueness validation for name should ignore case with self.assertRaises(ValidationError): vm2.full_clean() + + def test_disk_size(self): + vm = VirtualMachine( + cluster=Cluster.objects.first(), + name='Virtual Machine 1' + ) + vm.save() + vm.refresh_from_db() + self.assertEqual(vm.disk, None) + + # Create two VirtualDisks + VirtualDisk.objects.create(virtual_machine=vm, name='Virtual Disk 1', size=10) + VirtualDisk.objects.create(virtual_machine=vm, name='Virtual Disk 2', size=10) + vm.refresh_from_db() + self.assertEqual(vm.disk, 20) + + # Delete one VirtualDisk + VirtualDisk.objects.first().delete() + vm.refresh_from_db() + self.assertEqual(vm.disk, 10) + + # Attempt to manually overwrite the aggregate disk size + vm.disk = 30 + with self.assertRaises(ValidationError): + vm.full_clean()