Просмотр исходного кода

Closes #18658: Add start on boot field to VirtualMachine model (#20751)

RobertH1993 3 месяцев назад
Родитель
Сommit
01cbdbb968

+ 7 - 0
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.

+ 4 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -19,6 +19,10 @@
                     <th scope="row">{% trans "Status" %}</th>
                     <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
                 </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>
                     <th scope="row">{% trans "Role" %}</th>
                     <td>{{ object.role|linkify|placeholder }}</td>

+ 9 - 8
netbox/virtualization/api/serializers_/virtualmachines.py

@@ -31,6 +31,7 @@ __all__ = (
 
 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))

+ 14 - 0
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')
+    ]

+ 4 - 0
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(),

+ 7 - 1
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')),
     )

+ 8 - 2
netbox/virtualization/forms/bulk_import.py

@@ -89,6 +89,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(),
@@ -144,8 +150,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',
         )
 
 

+ 6 - 1
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,

+ 4 - 4
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):

+ 18 - 0
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),
+        ),
+    ]

+ 9 - 0
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 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     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:

+ 6 - 3
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',

+ 3 - 1
netbox/virtualization/tests/test_api.py

@@ -211,7 +211,8 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
                 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 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
             {
                 'name': 'Virtual Machine 7',
                 'cluster': clusters[2].pk,
+                'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_ON,
             },
         ]
 

+ 8 - 2
netbox/virtualization/tests/test_filtersets.py

@@ -349,7 +349,8 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
                 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 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
                 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 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
         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]}

+ 2 - 0
netbox/virtualization/tests/test_views.py

@@ -271,6 +271,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             '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 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'memory': 65535,
             'disk': 8000,
             'comments': 'New comments',
+            'start_on_boot': VirtualMachineStartOnBootChoices.STATUS_OFF,
         }
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])