nova-manage: Add libvirt update_machine_type command
This change adds a second update command to the libvirt group within nova-manage. This command will set or update the machine type of the instance when 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`` 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. blueprint: libvirt-default-machine-type Change-Id: I6b80021a2f90d3379c821dc8f02a72f350169eb3
This commit is contained in:
parent
20692c245c
commit
c70cde057d
@ -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
|
||||
========
|
||||
|
||||
|
@ -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='<instance_uuid>',
|
||||
help='UUID of instance to update')
|
||||
@args('machine_type', metavar='<machine_type>',
|
||||
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,
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user