Merge "nova-manage: Add libvirt update_machine_type command"
This commit is contained in:
commit
5890b142f4
|
@ -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…
Reference in New Issue