diff --git a/doc/source/cli/nova-manage.rst b/doc/source/cli/nova-manage.rst index 0495bda03dd9..6e9bda929f83 100644 --- a/doc/source/cli/nova-manage.rst +++ b/doc/source/cli/nova-manage.rst @@ -731,6 +731,50 @@ libvirt * - 3 - No machine type found for instance +``nova-manage libvirt update_machine_type [instance-uuid] [machine_type] [--force]`` + Set or update the recorded machine type of an instance. + + The following criteria must also be met when using this command: + + * The instance must have a ``vm_state`` of ``STOPPED``, ``SHELVED`` or + ``SHELVED_OFFLOADED``. + + * The machine type is supported. The supported list includes alias and + versioned types of ``pc``, ``pc-i440fx``, ``pc-q35``, ``q35``, ``virt`` + or ``s390-ccw-virtio``. + + * The update will not move the instance between underlying machine types. + For example, ``pc`` to ``q35``. + + * The update will not move the instance between an alias and versioned + machine type or vice versa. For example, ``pc`` to ``pc-1.2.3`` or + ``pc-1.2.3`` to ``pc``. + + A ``--force`` flag is provided to skip the above checks but caution + should be taken as this could easily lead to the underlying ABI of the + instance changing when moving between machine types. + + **Return Codes** + + .. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Return code + - Description + * - 0 + - Update completed successfully + * - 1 + - An unexpected error occurred + * - 2 + - Unable to find instance or instance mapping + * - 3 + - The instance has an invalid vm_state + * - 4 + - The proposed update of the machine type is invalid + * - 5 + - The provided machine type is unsupported + See Also ======== diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 77f2b8cb2bca..d0d993839188 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -2657,6 +2657,67 @@ class LibvirtCommands(object): LOG.exception('Unexpected error') return 1 + @action_description( + _("Set or update the stored machine type of the instance in the " + "database. This is only allowed for instances with a STOPPED, " + "SHELVED or SHELVED_OFFLOADED vm_state.")) + @args('instance_uuid', metavar='', + help='UUID of instance to update') + @args('machine_type', metavar='', + help='Machine type to set') + @args('--force', action='store_true', default=False, dest='force', + help='Force the update of the stored machine type') + def update_machine_type( + self, + instance_uuid=None, + machine_type=None, + force=False + ): + """Set or update the machine type of a given instance. + + Return codes: + + * 0: Command completed successfully. + * 1: An unexpected error happened. + * 2: Unable to find the instance or instance cell mapping. + * 3: Invalid instance vm_state. + * 4: Unable to move between underlying machine types (pc to q35 etc) + or to older versions. + * 5: Unsupported machine type. + """ + ctxt = context.get_admin_context() + if force: + print(_("Forcing update of machine type.")) + + try: + rtype, ptype = machine_type_utils.update_machine_type( + ctxt, instance_uuid, machine_type, force=force) + except exception.UnsupportedMachineType as e: + print(str(e)) + return 5 + except exception.InvalidMachineTypeUpdate as e: + print(str(e)) + return 4 + except exception.InstanceInvalidState as e: + print(str(e)) + return 3 + except ( + exception.InstanceNotFound, + exception.InstanceMappingNotFound, + ) as e: + print(str(e)) + return 2 + except Exception: + LOG.exception('Unexpected error') + return 1 + + print(_("Updated instance %(instance_uuid)s machine type to " + "%(machine_type)s (previously %(previous_type)s)") % + {'instance_uuid': instance_uuid, + 'machine_type': rtype, + 'previous_type': ptype}) + return 0 + CATEGORIES = { 'api_db': ApiDbCommands, diff --git a/nova/exception.py b/nova/exception.py index a74add6c40ab..c198686da791 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1751,6 +1751,15 @@ class InvalidMachineType(Invalid): "%(image_name)s (%(image_id)s): %(reason)s") +class InvalidMachineTypeUpdate(Invalid): + msg_fmt = _("Cannot update machine type %(existing_machine_type)s to " + "%(machine_type)s.") + + +class UnsupportedMachineType(Invalid): + msg_fmt = _("Machine type %(machine_type)s is not supported.") + + class InvalidVirtualMachineMode(Invalid): msg_fmt = _("Virtual machine mode '%(vmmode)s' is not recognised") diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 153965bff674..833c47a2e304 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -526,6 +526,14 @@ class InstanceHelperMixin: self._wait_for_migration_status(server, [expected_migration_status]) return self._wait_for_server_parameter(server, expected_result) + def _start_server(self, server): + self.api.post_server_action(server['id'], {'os-start': None}) + return self._wait_for_state_change(server, 'ACTIVE') + + def _stop_server(self, server): + self.api.post_server_action(server['id'], {'os-stop': None}) + return self._wait_for_state_change(server, 'SHUTOFF') + class PlacementHelperMixin: """A helper mixin for interacting with placement.""" diff --git a/nova/tests/functional/libvirt/test_machine_type.py b/nova/tests/functional/libvirt/test_machine_type.py index 7ebe063cddb4..b4a1efa61caf 100644 --- a/nova/tests/functional/libvirt/test_machine_type.py +++ b/nova/tests/functional/libvirt/test_machine_type.py @@ -16,6 +16,7 @@ import fixtures from oslo_utils.fixture import uuidsentinel from nova import context as nova_context +from nova import exception from nova import objects from nova.tests.functional.libvirt import base from nova.virt.libvirt import machine_type_utils @@ -199,3 +200,96 @@ class LibvirtMachineTypeTest(base.ServersTestBase): self.context, server_without['id'] ) ) + + def test_machine_type_update_stopped(self): + self.flags(hw_machine_type='x86_64=pc-1.2.3', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc-1.2.3') + + self._stop_server(server) + machine_type_utils.update_machine_type( + self.context, + server['id'], + 'pc-1.2.4' + ) + + self._start_server(server) + self._assert_machine_type(server['id'], 'pc-1.2.4') + + def test_machine_type_update_blocked_active(self): + self.flags(hw_machine_type='x86_64=pc-1.2.3', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc-1.2.3') + + self.assertRaises( + exception.InstanceInvalidState, + machine_type_utils.update_machine_type, + self.context, + server['id'], + 'pc-1.2.4' + ) + + def test_machine_type_update_blocked_between_alias_and_versioned(self): + self.flags(hw_machine_type='x86_64=pc', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc') + self._stop_server(server) + + self.assertRaises( + exception.InvalidMachineTypeUpdate, + machine_type_utils.update_machine_type, + self.context, + server['id'], + 'pc-1.2.4' + ) + + def test_machine_type_update_blocked_between_versioned_and_alias(self): + self.flags(hw_machine_type='x86_64=pc-1.2.3', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc-1.2.3') + self._stop_server(server) + + self.assertRaises( + exception.InvalidMachineTypeUpdate, + machine_type_utils.update_machine_type, + self.context, + server['id'], + 'pc' + ) + + def test_machine_type_update_blocked_between_types(self): + self.flags(hw_machine_type='x86_64=pc', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc') + self._stop_server(server) + + self.assertRaises( + exception.InvalidMachineTypeUpdate, + machine_type_utils.update_machine_type, + self.context, + server['id'], + 'q35' + ) + + def test_machine_type_update_force(self): + self.flags(hw_machine_type='x86_64=pc', group='libvirt') + + server = self._create_server(networks='none') + self._assert_machine_type(server['id'], 'pc') + + # Force through the update on an ACTIVE instance + machine_type_utils.update_machine_type( + self.context, + server['id'], + 'q35', + force=True + ) + + # Reboot the server so the config is updated so we can assert + self._reboot_server(server, hard=True) + self._assert_machine_type(server['id'], 'q35') diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index 6f764b9a4a6d..b3c138e058d6 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -3096,3 +3096,148 @@ class LibvirtCommandsTestCase(test.NoDBTestCase): self.assertEqual(3, ret) self.assertIn("No machine type registered for instance " f"{uuidsentinel.instance}", output) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context') + def test_update(self, mock_get_context, mock_update): + mock_update.return_value = ('pc-1.2', 'pc-1.1') + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type='pc-1.2' + ) + mock_update.assert_called_once_with( + mock_get_context.return_value, + uuidsentinel.instance, + 'pc-1.2', + force=False + ) + output = self.output.getvalue() + self.assertEqual(0, ret) + self.assertIn( + f"Updated instance {uuidsentinel.instance} machine type to pc-1.2 " + "(previously pc-1.1)", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context') + def test_update_force(self, mock_get_context, mock_update): + mock_update.return_value = ('q35', 'pc') + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type='q35', + force=True + ) + mock_update.assert_called_once_with( + mock_get_context.return_value, + uuidsentinel.instance, + 'q35', + force=True + ) + output = self.output.getvalue() + self.assertEqual(0, ret) + self.assertIn( + f"Updated instance {uuidsentinel.instance} machine type to q35 " + "(previously pc)", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_unknown_failure(self, mock_update): + mock_update.side_effect = Exception() + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + self.assertEqual(1, ret) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_instance_mapping_not_found(self, mock_update): + mock_update.side_effect = exception.InstanceMappingNotFound( + uuid=uuidsentinel.instance + ) + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + output = self.output.getvalue() + self.assertEqual(2, ret) + self.assertIn( + f"Instance {uuidsentinel.instance} has no mapping to a cell.", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_instance_not_found(self, mock_update): + mock_update.side_effect = exception.InstanceNotFound( + instance_id=uuidsentinel.instance + ) + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + output = self.output.getvalue() + self.assertEqual(2, ret) + self.assertIn( + f"Instance {uuidsentinel.instance} could not be found.", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_instance_invalid_state(self, mock_update): + mock_update.side_effect = exception.InstanceInvalidState( + instance_uuid=uuidsentinel.instance, + attr='vm_state', + state='ACTIVE', + method='update machine type' + ) + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + output = self.output.getvalue() + self.assertEqual(3, ret) + self.assertIn( + f"Instance {uuidsentinel.instance} in vm_state ACTIVE. Cannot " + "update machine type while the instance is in this state.", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_invalid_machine_type_update(self, mock_update): + mock_update.side_effect = exception.InvalidMachineTypeUpdate( + existing_machine_type='q35', + machine_type='pc', + ) + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + output = self.output.getvalue() + self.assertEqual(4, ret) + self.assertIn( + "Cannot update machine type q35 to pc.", + output + ) + + @mock.patch('nova.virt.libvirt.machine_type_utils.update_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_update_unsupported_machine_type(self, mock_update): + mock_update.side_effect = exception.UnsupportedMachineType( + machine_type='foo' + ) + ret = self.commands.update_machine_type( + instance_uuid=uuidsentinel.instance, + machine_type=mock.sentinel.machine_type + ) + output = self.output.getvalue() + self.assertEqual(5, ret) + self.assertIn( + "Machine type foo is not supported.", + output + ) diff --git a/nova/tests/unit/virt/libvirt/test_machine_type_utils.py b/nova/tests/unit/virt/libvirt/test_machine_type_utils.py index 16452d5bf8b5..6baf13832f95 100644 --- a/nova/tests/unit/virt/libvirt/test_machine_type_utils.py +++ b/nova/tests/unit/virt/libvirt/test_machine_type_utils.py @@ -10,15 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from oslo_utils.fixture import uuidsentinel from nova.compute import vm_states +from nova import exception from nova import objects from nova import test from nova.virt.libvirt import machine_type_utils +@ddt.ddt class TestMachineTypeUtils(test.NoDBTestCase): def _create_test_instance_obj( @@ -82,3 +85,213 @@ class TestMachineTypeUtils(test.NoDBTestCase): uuidsentinel.instance, expected_attrs=['system_metadata'] ) + + @ddt.data( + 'pc', + 'q35', + 'virt', + 's390-ccw-virtio', + 'pc-i440fx-2.12', + 'pc-q35-2.12', + 'virt-2.12', + 'pc-i440fx-rhel8.2.0', + 'pc-q35-rhel8.2.0') + def test_check_machine_type_support(self, machine_type): + # Assert UnsupportedMachineType isn't raised for supported types + machine_type_utils._check_machine_type_support( + machine_type) + + @ddt.data( + 'pc-foo', + 'pc-foo-1.2', + 'bar-q35', + 'virt-foo', + 'pc-virt') + def test_check_machine_type_support_failure(self, machine_type): + # Assert UnsupportedMachineType is raised for unsupported types + self.assertRaises( + exception.UnsupportedMachineType, + machine_type_utils._check_machine_type_support, + machine_type + ) + + @ddt.data( + ('pc-i440fx-2.10', 'pc-i440fx-2.11'), + ('pc-q35-2.10', 'pc-q35-2.11')) + def test_check_update_to_existing_type(self, machine_types): + # Assert that exception.InvalidMachineTypeUpdate is not raised when + # updating to the same type or between versions of the same type + original_type, update_type = machine_types + machine_type_utils._check_update_to_existing_type( + original_type, update_type) + + @ddt.data( + ('pc', 'q35'), + ('q35', 'pc'), + ('pc-i440fx-2.12', 'pc-q35-2.12'), + ('pc', 'pc-i440fx-2.12'), + ('pc-i440fx-2.12', 'pc'), + ('pc-i440fx-2.12', 'pc-i440fx-2.11')) + def test_check_update_to_existing_type_failure(self, machine_types): + # Assert that exception.InvalidMachineTypeUpdate is raised when + # updating to a different underlying machine type or between versioned + # and aliased machine types + existing_type, update_type = machine_types + self.assertRaises( + exception.InvalidMachineTypeUpdate, + machine_type_utils._check_update_to_existing_type, + existing_type, update_type + ) + + @ddt.data( + vm_states.STOPPED, + vm_states.SHELVED, + vm_states.SHELVED_OFFLOADED) + def test_check_vm_state(self, vm_state): + instance = self._create_test_instance_obj( + vm_state=vm_state + ) + machine_type_utils._check_vm_state(instance) + + @ddt.data( + vm_states.ACTIVE, + vm_states.PAUSED, + vm_states.ERROR) + def test_check_vm_state_failure(self, vm_state): + instance = self._create_test_instance_obj( + vm_state=vm_state + ) + self.assertRaises( + exception.InstanceInvalidState, + machine_type_utils._check_vm_state, + instance + ) + + @mock.patch('nova.objects.instance.Instance.save') + @mock.patch('nova.virt.libvirt.machine_type_utils._check_vm_state') + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.context.target_cell') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock(cell_mapping=mock.sentinel.cell_mapping)) + def test_update_noop( + self, + mock_target_cell, + mock_get_instance, + mock_check_vm_state, + mock_instance_save + ): + # Assert that update_machine_type is a noop when the type is already + # set within the instance, even if forced + existing_type = 'pc' + mock_target_cell.return_value.__enter__.return_value = ( + mock.sentinel.cell_context) + mock_get_instance.return_value = self._create_test_instance_obj( + mtype=existing_type, + ) + + self.assertEqual( + (existing_type, existing_type), + machine_type_utils.update_machine_type( + mock.sentinel.context, + instance_uuid=uuidsentinel.instance, + machine_type=existing_type + ), + ) + mock_check_vm_state.assert_not_called() + mock_instance_save.assert_not_called() + + self.assertEqual( + (existing_type, existing_type), + machine_type_utils.update_machine_type( + mock.sentinel.context, + instance_uuid=uuidsentinel.instance, + machine_type=existing_type, + force=True + ), + ) + mock_check_vm_state.assert_not_called() + mock_instance_save.assert_not_called() + + @ddt.data( + ('foobar', 'foobar', None), + ('foobar-1.3', 'foobar-1.3', 'foobar-1.2'), + ('foobar-1.2', 'foobar-1.2', 'foobar-1.3'), + ('foobar', 'foobar', 'q35'), + ('pc', 'pc', 'q35')) + @mock.patch('nova.objects.instance.Instance.save') + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.context.target_cell') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock(cell_mapping=mock.sentinel.cell_mapping)) + def test_update_force( + self, + types, + mock_target_cell, + mock_get_instance, + mock_instance_save + ): + expected_type, update_type, existing_type = types + mock_target_cell.return_value.__enter__.return_value = ( + mock.sentinel.cell_context) + instance = self._create_test_instance_obj( + mtype=existing_type + ) + mock_get_instance.return_value = instance + + returned_type = machine_type_utils.update_machine_type( + mock.sentinel.context, + uuidsentinel.instance, + machine_type=update_type, + force=True + ) + + # Assert that the instance machine type was updated and saved + self.assertEqual((expected_type, existing_type), returned_type) + self.assertEqual( + expected_type, + instance.system_metadata.get('image_hw_machine_type') + ) + mock_instance_save.assert_called_once() + + @ddt.data( + ('pc', 'pc', None), + ('q35', 'q35', None), + ('pc-1.2', 'pc-1.2', None), + ('pc-q35-1.2', 'pc-q35-1.2', None), + ('pc-1.2', 'pc-1.2', 'pc-1.1'), + ('pc-i440fx-1.2', 'pc-i440fx-1.2', 'pc-i440fx-1.1'), + ('pc-q35-1.2', 'pc-q35-1.2', 'pc-q35-1.1'), + ('pc-q35-rhel8.2.0', 'pc-q35-rhel8.2.0', 'pc-q35-rhel8.1.0')) + @mock.patch('nova.objects.instance.Instance.save') + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.context.target_cell') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock(cell_mapping=mock.sentinel.cell_mapping)) + def test_update( + self, + types, + mock_target_cell, + mock_get_instance, + mock_instance_save + ): + expected_type, update_type, existing_type = types + mock_target_cell.return_value.__enter__.return_value = ( + mock.sentinel.cell_context) + instance = self._create_test_instance_obj( + mtype=existing_type + ) + mock_get_instance.return_value = instance + + returned_type = machine_type_utils.update_machine_type( + mock.sentinel.context, + uuidsentinel.instance, + machine_type=update_type + ) + + # Assert that the instance machine type was updated and saved + self.assertEqual((expected_type, existing_type), returned_type) + self.assertEqual( + expected_type, + instance.system_metadata.get('image_hw_machine_type') + ) + mock_instance_save.assert_called_once() diff --git a/nova/virt/libvirt/machine_type_utils.py b/nova/virt/libvirt/machine_type_utils.py index db6ddd2dc6b0..70c25b9e8229 100644 --- a/nova/virt/libvirt/machine_type_utils.py +++ b/nova/virt/libvirt/machine_type_utils.py @@ -10,10 +10,39 @@ # License for the specific language governing permissions and limitations # under the License. +import re import typing as ty +from nova.compute import vm_states from nova import context as nova_context +from nova import exception from nova import objects +from oslo_utils import versionutils + + +SUPPORTED_TYPE_PATTERNS = [ + # As defined by nova.virt.libvirt.utils.get_default_machine_type + r'^pc$', + r'^q35$', + r'^virt$', + r'^s390-ccw-virtio$', + # versioned types of the above + r'^pc-\d+.\d+', + r'^pc-i440fx-\d+.\d+', + r'^pc-q35-\d+.\d+', + r'^virt-\d+.\d+', + r'^s390-ccw-virtio-\d+.\d+', + # RHEL specific versions of the pc-i440fx and pc-q35 types + r'^pc-i440fx-rhel\d.\d+.\d+', + r'^pc-q35-rhel\d.\d+.\d+', +] + + +ALLOWED_UPDATE_VM_STATES = [ + vm_states.STOPPED, + vm_states.SHELVED, + vm_states.SHELVED_OFFLOADED +] def get_machine_type( @@ -36,3 +65,116 @@ def get_machine_type( instance = objects.instance.Instance.get_by_uuid( cctxt, instance_uuid, expected_attrs=['system_metadata']) return instance.image_meta.properties.get('hw_machine_type') + + +def _check_machine_type_support( + mtype: str +) -> None: + """Check that the provided machine type is supported + + This check is done without access to the compute host and + so instead relies on a hardcoded list of supported machine types to + validate the provided machine type. + + :param machine_type: Machine type to check + :raises: nova.exception.UnsupportedMachineType + """ + if not any(m for m in SUPPORTED_TYPE_PATTERNS if re.match(m, mtype)): + raise exception.UnsupportedMachineType(machine_type=mtype) + + +def _check_update_to_existing_type( + existing_type: str, + machine_type: str +) -> None: + """Check the update to an existing machine type + + The aim here is to block operators from moving between the underying + machine types, between versioned and aliased types or to an older version + of the same type during an update. + + :param existing_type: The existing machine type + :param machine_type: The new machine type + :raises: nova.exception.InvalidMachineTypeUpdate + """ + # Check that we are not switching between types or between an alias and + # versioned type such as q35 to pc-q35-5.2.0 etc. + for m in SUPPORTED_TYPE_PATTERNS: + if re.match(m, existing_type) and not re.match(m, machine_type): + raise exception.InvalidMachineTypeUpdate( + existing_machine_type=existing_type, machine_type=machine_type) + + # Now check that the new version isn't older than the original. + # This needs to support x.y and x.y.z as used by RHEL shipped QEMU + version_pattern = r'\d+\.\d+$|\d+\.\d+\.\d+$' + if any(re.findall(version_pattern, existing_type)): + existing_version = re.findall(version_pattern, existing_type)[0] + new_version = re.findall(version_pattern, machine_type)[0] + if (versionutils.convert_version_to_int(new_version) < + versionutils.convert_version_to_int(existing_version)): + raise exception.InvalidMachineTypeUpdate( + existing_machine_type=existing_type, + machine_type=machine_type) + + +def _check_vm_state( + instance: 'objects.Instance', +): + """Ensure the vm_state of the instance is in ALLOWED_UPDATE_VM_STATES + + :param instance: Instance object to check + :raises: nova.exception.InstanceInvalidState + """ + if instance.vm_state not in ALLOWED_UPDATE_VM_STATES: + raise exception.InstanceInvalidState( + instance_uuid=instance.uuid, attr='vm_state', + state=instance.vm_state, method='update machine type.') + + +def update_machine_type( + context: nova_context.RequestContext, + instance_uuid: str, + machine_type: str, + force: bool = False, +) -> ty.Tuple[str, str]: + """Set or update the stored machine type of an instance + + :param instance_uuid: Instance UUID to update. + :param machine_type: Machine type to update. + :param force: If the update should be forced. + :returns: A tuple of the updated machine type and original machine type. + """ + im = objects.InstanceMapping.get_by_instance_uuid(context, instance_uuid) + with nova_context.target_cell(context, im.cell_mapping) as cctxt: + + instance = objects.instance.Instance.get_by_uuid( + cctxt, instance_uuid, expected_attrs=['system_metadata']) + + # Fetch the existing system metadata machine type if one is recorded + existing_mtype = instance.image_meta.properties.get('hw_machine_type') + + # Return if the type is already updated + if existing_mtype and existing_mtype == machine_type: + return machine_type, existing_mtype + + # If the caller wants to force the update now is the time to do it. + if force: + instance.system_metadata['image_hw_machine_type'] = machine_type + instance.save() + return machine_type, existing_mtype + + # Ensure the instance is in a suitable vm_state to update + _check_vm_state(instance) + + # Ensure the supplied machine type is supported + _check_machine_type_support(machine_type) + + # If the instance already has a type ensure the update is valid + if existing_mtype: + _check_update_to_existing_type(existing_mtype, machine_type) + + # Finally save the machine type in the instance system metadata + instance.system_metadata['image_hw_machine_type'] = machine_type + instance.save() + + return machine_type, existing_mtype diff --git a/releasenotes/notes/libvirt-store-and-change-default-machine-type-bf86b4973c4dee4c.yaml b/releasenotes/notes/libvirt-store-and-change-default-machine-type-bf86b4973c4dee4c.yaml index 2a4b1e77260e..7596ed8198e5 100644 --- a/releasenotes/notes/libvirt-store-and-change-default-machine-type-bf86b4973c4dee4c.yaml +++ b/releasenotes/notes/libvirt-store-and-change-default-machine-type-bf86b4973c4dee4c.yaml @@ -16,3 +16,27 @@ upgrade: This command will print the current machine type if set in the image metadata of the instance. + + ``nova-manage libvirt set_machine_type`` + + This command will set or update the machine type of the instance assuming + the following criteria are met: + + * The instance must have a ``vm_state`` of ``STOPPED``, ``SHELVED`` or + ``SHELVED_OFFLOADED``. + + * The machine type is supported. The supported list includes alias and + versioned types of ``pc``, ``pc-i440fx``, ``pc-q35``, ``q35``, ``virt``, + ``s390-ccw-virtio``, ``hyperv-gen1`` and ``hyperv-gen2`` as supported by + the hyperv driver. + + * The update will not move the instance between underlying machine types. + For example, ``pc`` to ``q35``. + + * The update will not move the instance between an alias and versioned + machine type or vice versa. For example, ``pc`` to ``pc-1.2.3`` or + ``pc-1.2.3`` to ``pc``. + + A ``--force`` flag is provided to skip the above checks but caution + should be taken as this could easily lead to the underlying ABI of the + instance changing when moving between machine types.