diff --git a/doc/source/cli/nova-manage.rst b/doc/source/cli/nova-manage.rst index 6e9bda929f83..2cc8d765e92f 100644 --- a/doc/source/cli/nova-manage.rst +++ b/doc/source/cli/nova-manage.rst @@ -775,6 +775,30 @@ libvirt * - 5 - The provided machine type is unsupported +``nova-manage libvirt list_unset_machine_type [--cell-uuid]`` + List the UUID of any instance without ``hw_machine_type`` set. + + This command is useful for operators attempting to determine when it is + safe to change the :oslo.config:option:`libvirt.hw_machine_type` option + within an environment. + + **Return Codes** + + .. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Return code + - Description + * - 0 + - Completed successfully, no instances found without hw_machine_type + * - 1 + - An unexpected error occurred + * - 2 + - Unable to find cell mapping + * - 3 + - Instances found without hw_machine_type set + See Also ======== diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index d0d993839188..73d2832c8465 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -2718,6 +2718,37 @@ class LibvirtCommands(object): 'previous_type': ptype}) return 0 + @action_description( + _("List the UUIDs of instances that do not have hw_machine_type set " + "in their image metadata")) + @args('--cell-uuid', metavar='', dest='cell_uuid', + required=False, help='UUID of cell from which to list instances') + def list_unset_machine_type(self, cell_uuid=None): + """List the UUIDs of instances without image_hw_machine_type set + + Return codes: + * 0: Command completed successfully, no instances found. + * 1: An unexpected error happened. + * 2: Unable to find cell mapping. + * 3: Instances found without hw_machine_type set. + """ + try: + instance_list = machine_type_utils.get_instances_without_type( + context.get_admin_context(), cell_uuid) + except exception.CellMappingNotFound as e: + print(str(e)) + return 2 + except Exception: + LOG.exception('Unexpected error') + return 1 + + if instance_list: + print('\n'.join(i.uuid for i in instance_list)) + return 3 + else: + print(_("No instances found without hw_machine_type set.")) + return 0 + CATEGORIES = { 'api_db': ApiDbCommands, diff --git a/nova/tests/functional/libvirt/test_machine_type.py b/nova/tests/functional/libvirt/test_machine_type.py index b4a1efa61caf..3507bf1a7d59 100644 --- a/nova/tests/functional/libvirt/test_machine_type.py +++ b/nova/tests/functional/libvirt/test_machine_type.py @@ -293,3 +293,15 @@ class LibvirtMachineTypeTest(base.ServersTestBase): # 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') + + def test_machine_type_list_unset_machine_type(self): + self.flags(hw_machine_type='x86_64=pc', group='libvirt') + + server_with, server_without = self._create_servers() + self._unset_machine_type(server_without['id']) + + instances = machine_type_utils.get_instances_without_type(self.context) + self.assertEqual( + server_without['id'], + instances[0].uuid + ) diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index b3c138e058d6..875345d35bf0 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -3241,3 +3241,74 @@ class LibvirtCommandsTestCase(test.NoDBTestCase): "Machine type foo is not supported.", output ) + + @mock.patch( + 'nova.virt.libvirt.machine_type_utils.get_instances_without_type') + @mock.patch('nova.context.get_admin_context') + def test_list_unset_machine_type_none_found( + self, mock_get_context, mock_get_instances + ): + mock_get_context.return_value = mock.sentinel.admin_context + mock_get_instances.return_value = [] + ret = self.commands.list_unset_machine_type( + cell_uuid=uuidsentinel.cell_uuid) + mock_get_instances.assert_called_once_with( + mock.sentinel.admin_context, + uuidsentinel.cell_uuid + ) + output = self.output.getvalue() + self.assertEqual(0, ret) + self.assertIn( + "No instances found without hw_machine_type set.", + output + ) + + @mock.patch( + 'nova.virt.libvirt.machine_type_utils.get_instances_without_type') + @mock.patch('nova.context.get_admin_context') + def test_list_unset_machine_type_unknown_failure( + self, mock_get_context, mock_get_instances + ): + mock_get_instances.side_effect = Exception() + ret = self.commands.list_unset_machine_type( + cell_uuid=uuidsentinel.cell_uuid) + self.assertEqual(1, ret) + + @mock.patch( + 'nova.virt.libvirt.machine_type_utils.get_instances_without_type') + @mock.patch('nova.context.get_admin_context') + def test_list_unset_machine_type_cell_mapping_not_found( + self, mock_get_context, mock_get_instances + ): + mock_get_context.return_value = mock.sentinel.admin_context + mock_get_instances.side_effect = exception.CellMappingNotFound( + uuid=uuidsentinel.cell_uuid + ) + ret = self.commands.list_unset_machine_type( + cell_uuid=uuidsentinel.cell_uuid) + output = self.output.getvalue() + self.assertEqual(2, ret) + self.assertIn( + f"Cell {uuidsentinel.cell_uuid} has no mapping", + output + ) + + @mock.patch( + 'nova.virt.libvirt.machine_type_utils.get_instances_without_type') + @mock.patch('nova.context.get_admin_context') + def test_list_unset_machine_type( + self, mock_get_context, mock_get_instances + ): + mock_get_context.return_value = mock.sentinel.admin_context + mock_get_instances.return_value = [ + mock.Mock(spec=objects.Instance, uuid=uuidsentinel.instance) + ] + ret = self.commands.list_unset_machine_type( + cell_uuid=uuidsentinel.cell_uuid) + mock_get_instances.assert_called_once_with( + mock.sentinel.admin_context, + uuidsentinel.cell_uuid + ) + output = self.output.getvalue() + self.assertEqual(3, ret) + self.assertIn(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 index 6baf13832f95..6ea68e64cc8e 100644 --- a/nova/tests/unit/virt/libvirt/test_machine_type_utils.py +++ b/nova/tests/unit/virt/libvirt/test_machine_type_utils.py @@ -13,11 +13,14 @@ import ddt import mock from oslo_utils.fixture import uuidsentinel +from oslo_utils import uuidutils from nova.compute import vm_states +from nova import context as nova_context from nova import exception from nova import objects from nova import test +from nova.tests import fixtures as nova_fixtures from nova.virt.libvirt import machine_type_utils @@ -295,3 +298,163 @@ class TestMachineTypeUtils(test.NoDBTestCase): instance.system_metadata.get('image_hw_machine_type') ) mock_instance_save.assert_called_once() + + +class TestMachineTypeUtilsListUnset(test.NoDBTestCase): + + USES_DB_SELF = True + NUMBER_OF_CELLS = 2 + + def setUp(self): + super().setUp() + self.useFixture(nova_fixtures.Database(database='api')) + self.context = nova_context.get_admin_context() + + @staticmethod + def _create_node_in_cell(ctxt, cell, hypervisor_type, nodename): + with nova_context.target_cell(ctxt, cell) as cctxt: + cn = objects.ComputeNode( + context=cctxt, + hypervisor_type=hypervisor_type, + hypervisor_hostname=nodename, + # The rest of these values are fakes. + host=uuidsentinel.host, + vcpus=4, + memory_mb=8 * 1024, + local_gb=40, + vcpus_used=2, + memory_mb_used=2 * 1024, + local_gb_used=10, + hypervisor_version=1, + cpu_info='{"arch": "x86_64"}') + cn.create() + return cn + + @staticmethod + def _create_instance_in_cell( + ctxt, + cell, + node, + is_deleted=False, + hw_machine_type=None + ): + with nova_context.target_cell(ctxt, cell) as cctxt: + inst = objects.Instance( + context=cctxt, + host=node.host, + node=node.hypervisor_hostname, + uuid=uuidutils.generate_uuid()) + inst.create() + + if hw_machine_type: + inst.system_metadata = { + 'image_hw_machine_type': hw_machine_type + } + + if is_deleted: + inst.destroy() + + return inst + + def _setup_instances(self): + # Setup the required cells + self._setup_cells() + + # Track and return the created instances so the tests can assert + instances = {} + + # Create a node in each cell + node1_cell1 = self._create_node_in_cell( + self.context, + self.cell_mappings['cell1'], + 'kvm', + uuidsentinel.node_cell1_uuid + ) + node2_cell2 = self._create_node_in_cell( + self.context, + self.cell_mappings['cell2'], + 'kvm', + uuidsentinel.node_cell2_uuid + ) + + # Create some instances with and without machine types defined in cell1 + instances['cell1_without_mtype'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell1'], + node1_cell1 + ) + instances['cell1_with_mtype'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell1'], + node1_cell1, + hw_machine_type='pc' + ) + instances['cell1_with_mtype_deleted'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell1'], + node1_cell1, + hw_machine_type='pc', + is_deleted=True + ) + + # Repeat for cell2 + instances['cell2_without_mtype'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell2'], + node2_cell2 + ) + instances['cell2_with_mtype'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell2'], + node2_cell2, + hw_machine_type='pc' + ) + instances['cell2_with_mtype_deleted'] = self._create_instance_in_cell( + self.context, + self.cell_mappings['cell2'], + node2_cell2, + hw_machine_type='pc', + is_deleted=True + ) + return instances + + def test_fresh_install_no_cell_mappings(self): + self.assertEqual( + [], + machine_type_utils.get_instances_without_type(self.context) + ) + + def test_fresh_install_no_computes(self): + self._setup_cells() + self.assertEqual( + [], + machine_type_utils.get_instances_without_type(self.context) + ) + + def test_get_from_specific_cell(self): + instances = self._setup_instances() + # Assert that we only see the uuid for instance cell1_without_mtype + instance_list = machine_type_utils.get_instances_without_type( + self.context, + cell_uuid=self.cell_mappings['cell1'].uuid + ) + self.assertEqual( + instances['cell1_without_mtype'].uuid, + instance_list[0].uuid + ) + + def test_get_multi_cell(self): + instances = self._setup_instances() + # Assert that we see both undeleted _without_mtype instances + instance_list = machine_type_utils.get_instances_without_type( + self.context, + ) + instance_uuids = [i.uuid for i in instance_list] + self.assertIn( + instances['cell1_without_mtype'].uuid, + instance_uuids + ) + self.assertIn( + instances['cell2_without_mtype'].uuid, + instance_uuids + ) diff --git a/nova/virt/libvirt/machine_type_utils.py b/nova/virt/libvirt/machine_type_utils.py index 70c25b9e8229..b05427e59bfa 100644 --- a/nova/virt/libvirt/machine_type_utils.py +++ b/nova/virt/libvirt/machine_type_utils.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools import re import typing as ty @@ -178,3 +179,48 @@ def update_machine_type( instance.save() return machine_type, existing_mtype + + +def _get_instances_without_mtype( + context: 'nova_context.RequestContext', +) -> ty.List[objects.instance.Instance]: + """Fetch a list of instance UUIDs from the DB without hw_machine_type set + + :param meta: 'sqlalchemy.MetaData' pointing to a given cell DB + :returns: A list of Instance objects or an empty list + """ + instances = objects.InstanceList.get_all( + context, expected_attrs=['system_metadata']) + instances_without = [] + for instance in instances: + if instance.deleted == 0 and instance.vm_state != vm_states.BUILDING: + if instance.image_meta.properties.get('hw_machine_type') is None: + instances_without.append(instance) + return instances_without + + +def get_instances_without_type( + context: 'nova_context.RequestContext', + cell_uuid: ty.Optional[str] = None, +) -> ty.List[objects.instance.Instance]: + """Find instances without hw_machine_type set, optionally within a cell. + + :param context: Request context + :param cell_uuid: Optional cell UUID to look within + :returns: A list of Instance objects or an empty list + """ + if cell_uuid: + cell_mapping = objects.CellMapping.get_by_uuid(context, cell_uuid) + results = nova_context.scatter_gather_single_cell( + context, + cell_mapping, + _get_instances_without_mtype + ) + + results = nova_context.scatter_gather_skip_cell0( + context, + _get_instances_without_mtype + ) + + # Flatten the returned list of results into a single list of instances + return list(itertools.chain(*[r for c, r in results.items()])) 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 7596ed8198e5..9bd952994625 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 @@ -40,3 +40,9 @@ upgrade: 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. + + ``nova-manage libvirt list_unset_machine_type`` + + This command will list instance UUIDs that do not have a machine type + recorded. An optional cell UUID can be provided to list on instances + without a machine type from that cell.