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 "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=['*'])