diff --git a/doc/source/cli/nova-manage.rst b/doc/source/cli/nova-manage.rst index b7e48804109e..0495bda03dd9 100644 --- a/doc/source/cli/nova-manage.rst +++ b/doc/source/cli/nova-manage.rst @@ -708,6 +708,28 @@ Placement * - 127 - Invalid input +libvirt +~~~~~~~ + +``nova-manage libvirt get_machine_type [instance-uuid]`` + Fetch and display the recorded machine type of a libvirt instance. + + **Return Codes** + + .. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Return code + - Description + * - 0 + - Successfully completed + * - 1 + - An unexpected error occurred + * - 2 + - Unable to find instance or instance mapping + * - 3 + - No machine type found for instance See Also ======== diff --git a/mypy-files.txt b/mypy-files.txt index 50700d848907..af7de8841b91 100644 --- a/mypy-files.txt +++ b/mypy-files.txt @@ -5,6 +5,7 @@ nova/scheduler/request_filter.py nova/scheduler/utils.py nova/virt/driver.py nova/virt/hardware.py +nova/virt/libvirt/machine_type_utils.py nova/virt/libvirt/__init__.py nova/virt/libvirt/driver.py nova/virt/libvirt/event.py diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 31e12ad1dc77..77f2b8cb2bca 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -67,6 +67,7 @@ from nova import rpc from nova.scheduler.client import report from nova.scheduler import utils as scheduler_utils from nova import version +from nova.virt.libvirt import machine_type_utils CONF = nova.conf.CONF LOG = logging.getLogger(__name__) @@ -2620,11 +2621,49 @@ class PlacementCommands(object): return 0 +class LibvirtCommands(object): + """Commands for managing libvirt instances""" + + @action_description( + _("Fetch the stored machine type of the instance from the database.")) + @args('instance_uuid', metavar='', + help='UUID of instance to fetch the machine type for') + def get_machine_type(self, instance_uuid=None): + """Fetch the stored machine type of the instance from the database. + + Return codes: + + * 0: Command completed successfully. + * 1: An unexpected error happened. + * 2: Unable to find instance or instance mapping. + * 3: No machine type found for the instance. + + """ + try: + ctxt = context.get_admin_context() + mtype = machine_type_utils.get_machine_type(ctxt, instance_uuid) + if mtype: + print(mtype) + return 0 + else: + print(_('No machine type registered for instance %s' % + instance_uuid)) + return 3 + except (exception.InstanceNotFound, + exception.InstanceMappingNotFound) as e: + print(str(e)) + return 2 + except Exception: + LOG.exception('Unexpected error') + return 1 + + CATEGORIES = { 'api_db': ApiDbCommands, 'cell_v2': CellV2Commands, 'db': DbCommands, - 'placement': PlacementCommands + 'placement': PlacementCommands, + 'libvirt': LibvirtCommands, } diff --git a/nova/tests/functional/libvirt/test_machine_type.py b/nova/tests/functional/libvirt/test_machine_type.py index cbcef574b5dc..7ebe063cddb4 100644 --- a/nova/tests/functional/libvirt/test_machine_type.py +++ b/nova/tests/functional/libvirt/test_machine_type.py @@ -18,6 +18,7 @@ from oslo_utils.fixture import uuidsentinel from nova import context as nova_context from nova import objects from nova.tests.functional.libvirt import base +from nova.virt.libvirt import machine_type_utils class LibvirtMachineTypeTest(base.ServersTestBase): @@ -78,6 +79,14 @@ class LibvirtMachineTypeTest(base.ServersTestBase): self.guest_configs[server_id].os_mach_type ) + def _unset_machine_type(self, server_id): + instance = objects.Instance.get_by_uuid( + self.context, + server_id, + ) + instance.system_metadata.pop('image_hw_machine_type') + instance.save() + def test_init_host_register_machine_type(self): """Assert that the machine type of an instance is recorded during init_host if not already captured by an image prop. @@ -91,12 +100,7 @@ class LibvirtMachineTypeTest(base.ServersTestBase): # Stop n-cpu and clear the recorded machine type from server_without to # allow init_host to register the machine type. self.computes['compute1'].stop() - instance_without = objects.Instance.get_by_uuid( - self.context, - server_without['id'], - ) - instance_without.system_metadata.pop('image_hw_machine_type') - instance_without.save() + self._unset_machine_type(server_without['id']) self.flags(hw_machine_type='x86_64=pc-q35-1.2.3', group='libvirt') @@ -178,3 +182,20 @@ class LibvirtMachineTypeTest(base.ServersTestBase): def test_machine_type_after_server_hard_reboot(self): self._test_machine_type_after_server_reboot(hard=True) + + def test_machine_type_get(self): + self.flags(hw_machine_type='x86_64=pc', group='libvirt') + + server_with, server_without = self._create_servers() + self.assertEqual( + 'q35', + machine_type_utils.get_machine_type( + self.context, server_with['id'] + ) + ) + self.assertEqual( + 'pc', + machine_type_utils.get_machine_type( + self.context, server_without['id'] + ) + ) diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index 29c9b6063dc5..6f764b9a4a6d 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -3019,3 +3019,80 @@ class TestNovaManageMain(test.NoDBTestCase): mock_conf.post_mortem = True self.assertEqual(255, manage.main()) self.assertTrue(mock_pm.called) + + +class LibvirtCommandsTestCase(test.NoDBTestCase): + + def setUp(self): + super().setUp() + self.output = StringIO() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.output)) + self.commands = manage.LibvirtCommands() + + @mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type') + @mock.patch('nova.context.get_admin_context') + def test_get(self, mock_get_context, mock_get_machine_type): + mock_get_context.return_value = mock.sentinel.admin_context + mock_get_machine_type.return_value = 'pc' + ret = self.commands.get_machine_type( + instance_uuid=uuidsentinel.instance + ) + mock_get_machine_type.assert_called_once_with( + mock.sentinel.admin_context, + uuidsentinel.instance + ) + output = self.output.getvalue() + self.assertEqual(0, ret) + self.assertIn('pc', output) + + @mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type') + @mock.patch('nova.context.get_admin_context') + def test_get_unknown_failure( + self, mock_get_context, mock_get_machine_type + ): + mock_get_machine_type.side_effect = Exception() + ret = self.commands.get_machine_type( + instance_uuid=uuidsentinel.instance + ) + self.assertEqual(1, ret) + + @mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_get_unable_to_find_instance_mapping(self, mock_get_machine_type): + mock_get_machine_type.side_effect = exception.InstanceMappingNotFound( + uuid=uuidsentinel.instance) + ret = self.commands.get_machine_type( + instance_uuid=uuidsentinel.instance + ) + 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.get_machine_type') + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_get_machine_type_unable_to_find_instance( + self, mock_get_machine_type + ): + mock_get_machine_type.side_effect = exception.InstanceNotFound( + instance_id=uuidsentinel.instance) + ret = self.commands.get_machine_type( + instance_uuid=uuidsentinel.instance) + 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.get_machine_type', + new=mock.Mock(return_value=None)) + @mock.patch('nova.context.get_admin_context', new=mock.Mock()) + def test_get_none_found(self): + ret = self.commands.get_machine_type( + instance_uuid=uuidsentinel.instance + ) + output = self.output.getvalue() + self.assertEqual(3, ret) + self.assertIn("No machine type registered for instance " + f"{uuidsentinel.instance}", 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 new file mode 100644 index 000000000000..16452d5bf8b5 --- /dev/null +++ b/nova/tests/unit/virt/libvirt/test_machine_type_utils.py @@ -0,0 +1,84 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_utils.fixture import uuidsentinel + +from nova.compute import vm_states +from nova import objects +from nova import test +from nova.virt.libvirt import machine_type_utils + + +class TestMachineTypeUtils(test.NoDBTestCase): + + def _create_test_instance_obj( + self, + vm_state=vm_states.STOPPED, + mtype=None + ): + instance = objects.Instance( + uuid=uuidsentinel.instance, host='fake', node='fake', + task_state=None, flavor=objects.Flavor(), + project_id='fake-project', user_id='fake-user', + vm_state=vm_state, system_metadata={} + ) + if mtype: + instance.system_metadata = { + 'image_hw_machine_type': mtype, + } + return instance + + @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_get_machine_type(self, mock_target_cell, mock_get_instance): + mock_target_cell.return_value.__enter__.return_value = ( + mock.sentinel.cell_context) + mock_get_instance.return_value = self._create_test_instance_obj( + mtype='pc' + ) + self.assertEqual( + 'pc', + machine_type_utils.get_machine_type( + mock.sentinel.context, + instance_uuid=uuidsentinel.instance + ) + ) + mock_get_instance.assert_called_once_with( + mock.sentinel.cell_context, + uuidsentinel.instance, + expected_attrs=['system_metadata'] + ) + + @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_get_machine_type_none_found( + self, mock_target_cell, mock_get_instance + ): + mock_target_cell.return_value.__enter__.return_value = ( + mock.sentinel.cell_context) + mock_get_instance.return_value = self._create_test_instance_obj() + self.assertIsNone( + machine_type_utils.get_machine_type( + mock.sentinel.context, + instance_uuid=uuidsentinel.instance + ) + ) + mock_get_instance.assert_called_once_with( + mock.sentinel.cell_context, + uuidsentinel.instance, + expected_attrs=['system_metadata'] + ) diff --git a/nova/virt/libvirt/machine_type_utils.py b/nova/virt/libvirt/machine_type_utils.py new file mode 100644 index 000000000000..db6ddd2dc6b0 --- /dev/null +++ b/nova/virt/libvirt/machine_type_utils.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import typing as ty + +from nova import context as nova_context +from nova import objects + + +def get_machine_type( + context: 'nova_context.RequestContext', + instance_uuid: str, +) -> ty.Optional[str]: + """Get the registered machine type of an instance + + :param context: Request context. + :param instance_uuid: Instance UUID to check. + :returns: The machine type or None. + :raises: exception.InstanceNotFound, exception.InstanceMappingNotFound + """ + im = objects.InstanceMapping.get_by_instance_uuid(context, instance_uuid) + with nova_context.target_cell(context, im.cell_mapping) as cctxt: + # NOTE(lyarwood): While we are after the value of 'hw_machine_type' + # stored as an image metadata property this actually comes from the + # system metadata of the instance so we need to add + # expected_attrs=['system_metadata'] here. + instance = objects.instance.Instance.get_by_uuid( + cctxt, instance_uuid, expected_attrs=['system_metadata']) + return instance.image_meta.properties.get('hw_machine_type') 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 e440e6bf8f63..2a4b1e77260e 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 @@ -8,3 +8,11 @@ upgrade: This machine type will then be used when the instance is restarted or migrated as it will now appear as an image metadata property associated with the instance. + + The following new ``nova-manage`` commands have been introduced to help + operators manage the ``hw_machine_type`` image property: + + ``nova-manage libvirt get_machine_type`` + + This command will print the current machine type if set in the image + metadata of the instance.