Closes #18658: Add start on boot field to VirtualMachine model (#20751)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled

This commit is contained in:
RobertH1993 2025-11-12 20:59:01 +01:00 committed by GitHub
parent a4365be0a3
commit 01cbdbb968
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 109 additions and 22 deletions

View File

@ -21,6 +21,13 @@ The VM's operational status.
!!! tip !!! tip
Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Start on boot
The start on boot setting from the hypervisor.
!!! tip
Additional statuses may be defined by setting `VirtualMachine.start_on_boot` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Site & Cluster ### Site & Cluster
The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned. The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned.

View File

@ -19,6 +19,10 @@
<th scope="row">{% trans "Status" %}</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Start on boot" %}</th>
<td>{% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %}</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Role" %}</th> <th scope="row">{% trans "Role" %}</th>
<td>{{ object.role|linkify|placeholder }}</td> <td>{{ object.role|linkify|placeholder }}</td>

View File

@ -31,6 +31,7 @@ __all__ = (
class VirtualMachineSerializer(PrimaryModelSerializer): class VirtualMachineSerializer(PrimaryModelSerializer):
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
start_on_boot = ChoiceField(choices=VirtualMachineStartOnBootChoices, required=False)
site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
cluster = ClusterSerializer(nested=True, required=False, allow_null=True, default=None) cluster = ClusterSerializer(nested=True, required=False, allow_null=True, default=None)
device = DeviceSerializer(nested=True, required=False, allow_null=True, default=None) device = DeviceSerializer(nested=True, required=False, allow_null=True, default=None)
@ -49,10 +50,10 @@ class VirtualMachineSerializer(PrimaryModelSerializer):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'serial', 'role', 'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device',
'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
'owner', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'disk', 'description', 'owner', 'comments', 'config_template', 'local_context_data', 'tags',
'last_updated', 'interface_count', 'virtual_disk_count', 'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
@ -62,10 +63,10 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
class Meta(VirtualMachineSerializer.Meta): class Meta(VirtualMachineSerializer.Meta):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'serial', 'role', 'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device',
'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'disk', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
'last_updated', 'interface_count', 'virtual_disk_count', 'config_context', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
] ]
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))

View File

@ -49,3 +49,17 @@ class VirtualMachineStatusChoices(ChoiceSet):
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'), (STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
(STATUS_PAUSED, _('Paused'), 'orange'), (STATUS_PAUSED, _('Paused'), 'orange'),
] ]
class VirtualMachineStartOnBootChoices(ChoiceSet):
key = 'VirtualMachine.start_on_boot'
STATUS_ON = 'on'
STATUS_OFF = 'off'
STATUS_LAST_STATE = 'laststate'
CHOICES = [
(STATUS_ON, _('On'), 'green'),
(STATUS_OFF, _('Off'), 'gray'),
(STATUS_LAST_STATE, _('Last State'), 'cyan')
]

View File

@ -92,6 +92,10 @@ class VirtualMachineFilterSet(
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
null_value=None null_value=None
) )
start_on_boot = django_filters.MultipleChoiceFilter(
choices=VirtualMachineStartOnBootChoices,
null_value=None
)
cluster_group_id = django_filters.ModelMultipleChoiceFilter( cluster_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='cluster__group', field_name='cluster__group',
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),

View File

@ -85,6 +85,12 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
required=False, required=False,
initial='', initial='',
) )
start_on_boot = forms.ChoiceField(
label=_('Start on boot'),
choices=add_blank_choice(VirtualMachineStartOnBootChoices),
required=False,
initial='',
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'), label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -145,7 +151,7 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
model = VirtualMachine model = VirtualMachine
fieldsets = ( fieldsets = (
FieldSet('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description'), FieldSet('site', 'cluster', 'device', 'status', 'start_on_boot', 'role', 'tenant', 'platform', 'description'),
FieldSet('vcpus', 'memory', 'disk', name=_('Resources')), FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
FieldSet('config_template', name=_('Configuration')), FieldSet('config_template', name=_('Configuration')),
) )

View File

@ -89,6 +89,12 @@ class VirtualMachineImportForm(PrimaryModelImportForm):
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
start_on_boot = CSVChoiceField(
label=_('Start on boot'),
choices=VirtualMachineStartOnBootChoices,
help_text=_('Start on boot in hypervisor'),
required=False,
)
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'), label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -144,8 +150,8 @@ class VirtualMachineImportForm(PrimaryModelImportForm):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'name', 'status', 'start_on_boot', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus',
'description', 'serial', 'config_template', 'comments', 'owner', 'tags', 'memory', 'disk', 'description', 'serial', 'config_template', 'comments', 'owner', 'tags',
) )

View File

@ -109,7 +109,7 @@ class VirtualMachineFilterForm(
FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')), FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet( FieldSet(
'status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'status', 'start_on_boot', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id',
'local_context_data', 'serial', name=_('Attributes') 'local_context_data', 'serial', name=_('Attributes')
), ),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
@ -171,6 +171,11 @@ class VirtualMachineFilterForm(
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
required=False required=False
) )
start_on_boot = forms.MultipleChoiceField(
label=_('Start on boot'),
choices=VirtualMachineStartOnBootChoices,
required=False
)
platform_id = DynamicModelMultipleChoiceField( platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False, required=False,

View File

@ -217,7 +217,7 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
) )
fieldsets = ( fieldsets = (
FieldSet('name', 'role', 'status', 'description', 'serial', 'tags', name=_('Virtual Machine')), FieldSet('name', 'role', 'status', 'start_on_boot', 'description', 'serial', 'tags', name=_('Virtual Machine')),
FieldSet('site', 'cluster', 'device', name=_('Site/Cluster')), FieldSet('site', 'cluster', 'device', name=_('Site/Cluster')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')), FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')),
@ -228,9 +228,9 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = [ fields = [
'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner', 'comments', 'tags', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner',
'local_context_data', 'config_template', 'comments', 'tags', 'local_context_data', 'config_template',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-05 13:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0049_owner'),
]
operations = [
migrations.AddField(
model_name='virtualmachine',
name='start_on_boot',
field=models.CharField(default='off', max_length=32),
),
]

View File

@ -79,6 +79,12 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
default=VirtualMachineStatusChoices.STATUS_ACTIVE, default=VirtualMachineStatusChoices.STATUS_ACTIVE,
verbose_name=_('status') verbose_name=_('status')
) )
start_on_boot = models.CharField(
max_length=32,
choices=VirtualMachineStartOnBootChoices,
default=VirtualMachineStartOnBootChoices.STATUS_OFF,
verbose_name=_('start on boot'),
)
role = models.ForeignKey( role = models.ForeignKey(
to='dcim.DeviceRole', to='dcim.DeviceRole',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -247,6 +253,9 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
def get_status_color(self): def get_status_color(self):
return VirtualMachineStatusChoices.colors.get(self.status) return VirtualMachineStatusChoices.colors.get(self.status)
def get_start_on_boot_color(self):
return VirtualMachineStartOnBootChoices.colors.get(self.start_on_boot)
@property @property
def primary_ip(self): def primary_ip(self):
if get_config().PREFER_IPV4 and self.primary_ip4: if get_config().PREFER_IPV4 and self.primary_ip4:

View File

@ -29,6 +29,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
status = columns.ChoiceFieldColumn( status = columns.ChoiceFieldColumn(
verbose_name=_('Status'), verbose_name=_('Status'),
) )
start_on_boot = columns.ChoiceFieldColumn(
verbose_name=_('Start on boot'),
)
site = tables.Column( site = tables.Column(
verbose_name=_('Site'), verbose_name=_('Site'),
linkify=True linkify=True
@ -81,9 +84,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
class Meta(PrimaryModelTable.Meta): class Meta(PrimaryModelTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'vcpus', 'pk', 'id', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant',
'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments', 'config_template', 'tenant_group', 'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description',
'serial', 'contacts', 'tags', 'created', 'last_updated', 'comments', 'config_template', 'serial', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', 'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',

View File

@ -211,7 +211,8 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
name='Virtual Machine 3', name='Virtual Machine 3',
site=sites[0], site=sites[0],
cluster=clusters[0], cluster=clusters[0],
local_context_data={'C': 3} local_context_data={'C': 3},
start_on_boot=VirtualMachineStartOnBootChoices.STATUS_ON,
), ),
) )
VirtualMachine.objects.bulk_create(virtual_machines) VirtualMachine.objects.bulk_create(virtual_machines)
@ -235,6 +236,7 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'Virtual Machine 7', 'name': 'Virtual Machine 7',
'cluster': clusters[2].pk, 'cluster': clusters[2].pk,
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON,
}, },
] ]

View File

@ -349,7 +349,8 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
memory=2, memory=2,
disk=2, disk=2,
description='foobar2', description='foobar2',
serial='222-bbb' serial='222-bbb',
start_on_boot=VirtualMachineStartOnBootChoices.STATUS_OFF,
), ),
VirtualMachine( VirtualMachine(
name='Virtual Machine 3', name='Virtual Machine 3',
@ -363,7 +364,8 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
vcpus=3, vcpus=3,
memory=3, memory=3,
disk=3, disk=3,
description='foobar3' description='foobar3',
start_on_boot=VirtualMachineStartOnBootChoices.STATUS_ON,
), ),
) )
VirtualMachine.objects.bulk_create(vms) VirtualMachine.objects.bulk_create(vms)
@ -430,6 +432,10 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [VirtualMachineStatusChoices.STATUS_ACTIVE, VirtualMachineStatusChoices.STATUS_STAGED]} params = {'status': [VirtualMachineStatusChoices.STATUS_ACTIVE, VirtualMachineStatusChoices.STATUS_STAGED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_start_on_boot(self):
params = {'start_on_boot': [VirtualMachineStartOnBootChoices.STATUS_ON]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_cluster_group(self): def test_cluster_group(self):
groups = ClusterGroup.objects.all()[:2] groups = ClusterGroup.objects.all()[:2]
params = {'cluster_group_id': [groups[0].pk, groups[1].pk]} params = {'cluster_group_id': [groups[0].pk, groups[1].pk]}

View File

@ -271,6 +271,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'platform': platforms[1].pk, 'platform': platforms[1].pk,
'name': 'Virtual Machine X', 'name': 'Virtual Machine X',
'status': VirtualMachineStatusChoices.STATUS_STAGED, 'status': VirtualMachineStatusChoices.STATUS_STAGED,
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON,
'role': roles[1].pk, 'role': roles[1].pk,
'primary_ip4': None, 'primary_ip4': None,
'primary_ip6': None, 'primary_ip6': None,
@ -309,6 +310,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'memory': 65535, 'memory': 65535,
'disk': 8000, 'disk': 8000,
'comments': 'New comments', 'comments': 'New comments',
'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_OFF,
} }
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])