diff --git a/docs/models/virtualization/virtualmachine.md b/docs/models/virtualization/virtualmachine.md index a90b2752d6d..189a4ba7566 100644 --- a/docs/models/virtualization/virtualmachine.md +++ b/docs/models/virtualization/virtualmachine.md @@ -21,6 +21,13 @@ The VM's operational status. !!! tip 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 The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned. diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 7f902a20ded..1ee566eb0b8 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -19,6 +19,10 @@

{% trans "Virtual Machine" %}

{% trans "Status" %} {% badge object.get_status_display bg_color=object.get_status_color %} + + {% trans "Start on boot" %} + {% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %} + {% trans "Role" %} {{ object.role|linkify|placeholder }} diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py index c035a436a0a..25030eaf3f9 100644 --- a/netbox/virtualization/api/serializers_/virtualmachines.py +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -31,6 +31,7 @@ class VirtualMachineSerializer(PrimaryModelSerializer): 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) cluster = ClusterSerializer(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: model = VirtualMachine fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'serial', 'role', - 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', - 'owner', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', - 'last_updated', 'interface_count', 'virtual_disk_count', + 'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', + 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', + 'disk', 'description', 'owner', 'comments', 'config_template', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') @@ -62,10 +63,10 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class Meta(VirtualMachineSerializer.Meta): fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'serial', 'role', - 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', - 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', - 'last_updated', 'interface_count', 'virtual_disk_count', + 'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', + 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', + 'disk', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', + 'config_context', 'created', 'last_updated', 'interface_count', 'virtual_disk_count', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/virtualization/choices.py b/netbox/virtualization/choices.py index b60a6e1ff34..b00c9f1e26b 100644 --- a/netbox/virtualization/choices.py +++ b/netbox/virtualization/choices.py @@ -49,3 +49,17 @@ class VirtualMachineStatusChoices(ChoiceSet): (STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'), (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') + ] diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index e2ef8cb6a8a..b96f1dc24e9 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -92,6 +92,10 @@ class VirtualMachineFilterSet( choices=VirtualMachineStatusChoices, null_value=None ) + start_on_boot = django_filters.MultipleChoiceFilter( + choices=VirtualMachineStartOnBootChoices, + null_value=None + ) cluster_group_id = django_filters.ModelMultipleChoiceFilter( field_name='cluster__group', queryset=ClusterGroup.objects.all(), diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 092bf576b0a..b8a7f0c10db 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -85,6 +85,12 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): required=False, initial='', ) + start_on_boot = forms.ChoiceField( + label=_('Start on boot'), + choices=add_blank_choice(VirtualMachineStartOnBootChoices), + required=False, + initial='', + ) site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -145,7 +151,7 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): model = VirtualMachine 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('config_template', name=_('Configuration')), ) diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index 67f39b6f531..10b973e8cb0 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -88,6 +88,12 @@ class VirtualMachineImportForm(PrimaryModelImportForm): choices=VirtualMachineStatusChoices, 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( label=_('Site'), queryset=Site.objects.all(), @@ -143,8 +149,8 @@ class VirtualMachineImportForm(PrimaryModelImportForm): class Meta: model = VirtualMachine fields = ( - 'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', - 'description', 'serial', 'config_template', 'comments', 'owner', 'tags', + 'name', 'status', 'start_on_boot', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', + 'memory', 'disk', 'description', 'serial', 'config_template', 'comments', 'owner', 'tags', ) diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 3e0db175e4f..27fda4a85a6 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -109,7 +109,7 @@ class VirtualMachineFilterForm( FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), 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') ), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), @@ -171,6 +171,11 @@ class VirtualMachineFilterForm( choices=VirtualMachineStatusChoices, required=False ) + start_on_boot = forms.MultipleChoiceField( + label=_('Start on boot'), + choices=VirtualMachineStartOnBootChoices, + required=False + ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index fa4966b2b3b..e3ba36bad04 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -217,7 +217,7 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm): ) 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('tenant_group', 'tenant', name=_('Tenancy')), FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')), @@ -228,9 +228,9 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', - 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner', 'comments', 'tags', - 'local_context_data', 'config_template', + 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', + 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner', + 'comments', 'tags', 'local_context_data', 'config_template', ] def __init__(self, *args, **kwargs): diff --git a/netbox/virtualization/migrations/0050_virtualmachine_start_on_boot.py b/netbox/virtualization/migrations/0050_virtualmachine_start_on_boot.py new file mode 100644 index 00000000000..899cb28a8a0 --- /dev/null +++ b/netbox/virtualization/migrations/0050_virtualmachine_start_on_boot.py @@ -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), + ), + ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index de6fde745f9..f4679c9c2ff 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -79,6 +79,12 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co default=VirtualMachineStatusChoices.STATUS_ACTIVE, 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( to='dcim.DeviceRole', on_delete=models.PROTECT, @@ -247,6 +253,9 @@ def save(self, *args, **kwargs): def get_status_color(self): return VirtualMachineStatusChoices.colors.get(self.status) + def get_start_on_boot_color(self): + return VirtualMachineStartOnBootChoices.colors.get(self.start_on_boot) + @property def primary_ip(self): if get_config().PREFER_IPV4 and self.primary_ip4: diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index fcb9017df50..c770581d02e 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -29,6 +29,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel status = columns.ChoiceFieldColumn( verbose_name=_('Status'), ) + start_on_boot = columns.ChoiceFieldColumn( + verbose_name=_('Start on boot'), + ) site = tables.Column( verbose_name=_('Site'), linkify=True @@ -81,9 +84,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel class Meta(PrimaryModelTable.Meta): model = VirtualMachine fields = ( - 'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'vcpus', - 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments', 'config_template', - 'serial', 'contacts', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'role', 'tenant', + 'tenant_group', 'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', + 'comments', 'config_template', 'serial', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 56f9132abe3..40c4df2ba3b 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -211,7 +211,8 @@ def setUpTestData(cls): name='Virtual Machine 3', site=sites[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) @@ -235,6 +236,7 @@ def setUpTestData(cls): { 'name': 'Virtual Machine 7', 'cluster': clusters[2].pk, + 'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON, }, ] diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index 0179069af06..13a007c1539 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -349,7 +349,8 @@ def setUpTestData(cls): memory=2, disk=2, description='foobar2', - serial='222-bbb' + serial='222-bbb', + start_on_boot=VirtualMachineStartOnBootChoices.STATUS_OFF, ), VirtualMachine( name='Virtual Machine 3', @@ -363,7 +364,8 @@ def setUpTestData(cls): vcpus=3, memory=3, disk=3, - description='foobar3' + description='foobar3', + start_on_boot=VirtualMachineStartOnBootChoices.STATUS_ON, ), ) VirtualMachine.objects.bulk_create(vms) @@ -430,6 +432,10 @@ def test_status(self): params = {'status': [VirtualMachineStatusChoices.STATUS_ACTIVE, VirtualMachineStatusChoices.STATUS_STAGED]} 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): groups = ClusterGroup.objects.all()[:2] params = {'cluster_group_id': [groups[0].pk, groups[1].pk]} diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 35226c16dc3..556dd6f8856 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -271,6 +271,7 @@ def setUpTestData(cls): 'platform': platforms[1].pk, 'name': 'Virtual Machine X', 'status': VirtualMachineStatusChoices.STATUS_STAGED, + 'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON, 'role': roles[1].pk, 'primary_ip4': None, 'primary_ip6': None, @@ -309,6 +310,7 @@ def setUpTestData(cls): 'memory': 65535, 'disk': 8000, 'comments': 'New comments', + 'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_OFF, } @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])