From f3730dcc81dfd0034e7ccc744cda368a7d05455a Mon Sep 17 00:00:00 2001 From: Fredrik Hindersson Date: Wed, 10 Jun 2026 09:32:04 +0000 Subject: [PATCH 1/2] Fix autostart-on-host-failure tag check never matching instance.tags is a TagList of Tag objects, so testing membership with "autostart_instance_on_host_failure" in instance.tags compared the string against Tag objects and never matched. tag_state was therefore always False, which made expect_running always False, so no instance was ever resumed after a host reboot -- the opposite of the intended opt-in autostart behaviour. Compare against the tag names instead, and add unit tests covering the tagged and untagged cases. The existing failed-resume test is updated to tag its instance so it still reaches the resume path now that resume is gated on the tag. --- nova/compute/manager.py | 5 +- nova/tests/unit/compute/test_compute_mgr.py | 64 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5440c22dd3a..98e1f5a9467 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1296,9 +1296,8 @@ def _init_instance(self, context, instance): db_state = instance.power_state drv_state = self._get_power_state(instance) - tag_state = False - if "autostart_instance_on_host_failure" in instance.tags: - tag_state = True + tag_state = "autostart_instance_on_host_failure" in ( + [tag.tag for tag in instance.tags]) expect_running = (db_state == power_state.RUNNING and drv_state != db_state and tag_state) diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 0d39a570c02..92c34d3e5a2 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -1544,6 +1544,10 @@ def test_init_instance_failed_resume_sets_error(self, mock_set_inst, task_state=None, host=self.compute.host, expected_attrs=['info_cache']) + # Resume on host boot is gated on the autostart tag, so the instance + # must carry it to reach the resume path being exercised here. + instance.tags = objects.TagList( + objects=[objects.Tag(tag='autostart_instance_on_host_failure')]) self.flags(resume_guests_state_on_host_boot=True) mock_get_power.side_effect = (power_state.SHUTDOWN, @@ -1559,6 +1563,66 @@ def test_init_instance_failed_resume_sets_error(self, mock_set_inst, 'fake-bdm') mock_set_inst.assert_called_once_with(instance) + @mock.patch.object(manager.ComputeManager, '_get_power_state') + @mock.patch.object(fake_driver.FakeDriver, 'plug_vifs') + @mock.patch.object(fake_driver.FakeDriver, 'resume_state_on_host_boot') + @mock.patch.object(manager.ComputeManager, + '_get_instance_block_device_info') + def test_init_instance_resumes_tagged_instance( + self, mock_get_inst, mock_resume, mock_plug, mock_get_power): + # An instance carrying the autostart_instance_on_host_failure tag that + # was running but is now down must be resumed on host boot. + instance = fake_instance.fake_instance_obj( + self.context, + uuid=uuids.instance, + info_cache=None, + power_state=power_state.RUNNING, + vm_state=vm_states.ACTIVE, + task_state=None, + host=self.compute.host, + expected_attrs=['info_cache']) + instance.tags = objects.TagList( + objects=[objects.Tag(tag='autostart_instance_on_host_failure')]) + + self.flags(resume_guests_state_on_host_boot=True) + mock_get_power.side_effect = (power_state.SHUTDOWN, + power_state.SHUTDOWN) + mock_get_inst.return_value = 'fake-bdm' + + self.compute._init_instance('fake-context', instance) + + mock_resume.assert_called_once_with(mock.ANY, instance, mock.ANY, + 'fake-bdm') + + @mock.patch.object(manager.ComputeManager, '_get_power_state') + @mock.patch.object(fake_driver.FakeDriver, 'plug_vifs') + @mock.patch.object(fake_driver.FakeDriver, 'resume_state_on_host_boot') + @mock.patch.object(manager.ComputeManager, + '_get_instance_block_device_info') + def test_init_instance_does_not_resume_untagged_instance( + self, mock_get_inst, mock_resume, mock_plug, mock_get_power): + # Without the autostart_instance_on_host_failure tag an instance must + # not be resumed, even if it was running before the host went down. + instance = fake_instance.fake_instance_obj( + self.context, + uuid=uuids.instance, + info_cache=None, + power_state=power_state.RUNNING, + vm_state=vm_states.ACTIVE, + task_state=None, + host=self.compute.host, + expected_attrs=['info_cache']) + instance.tags = objects.TagList(objects=[]) + + self.flags(resume_guests_state_on_host_boot=True) + mock_get_power.side_effect = (power_state.SHUTDOWN, + power_state.SHUTDOWN) + mock_get_inst.return_value = 'fake-bdm' + + self.compute._init_instance('fake-context', instance) + + mock_resume.assert_not_called() + @mock.patch.object(objects.BlockDeviceMapping, 'destroy') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') @mock.patch.object(objects.Instance, 'destroy') From e8337f73e76835c9134c7b92355970a7af2139e8 Mon Sep 17 00:00:00 2001 From: Fredrik Hindersson Date: Wed, 10 Jun 2026 09:34:58 +0000 Subject: [PATCH 2/2] Load instance tags eagerly during host init _init_instance now reads instance.tags for every instance to decide whether to resume it on host boot. tags was not in the expected_attrs passed to InstanceList.get_by_host, so each access triggered a lazy load -- one extra DB query per instance during nova-compute startup. Add 'tags' to expected_attrs so they are joined in up front. --- nova/compute/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 98e1f5a9467..1e72f971eee 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1529,7 +1529,7 @@ def init_host(self): context = nova.context.get_admin_context() instances = objects.InstanceList.get_by_host( context, self.host, - expected_attrs=['info_cache', 'metadata', 'numa_topology']) + expected_attrs=['info_cache', 'metadata', 'numa_topology', 'tags']) self.init_virt_events()