From 64ecc88972dafdf463d98a39b79aecbfabf5eaf8 Mon Sep 17 00:00:00 2001 From: Alfredo Moralejo Date: Mon, 23 Feb 2026 12:28:55 +0100 Subject: [PATCH] Add get_flavor_id method to nova_helper The get_flavor_id method is extracted from resize_instance to improve code reusability. That method will be used in the pre_condition of resize action. Add comprehensive unit tests for the get_flavor_id method in nova_helper. Assisted-By: Claude (Sonnet 4.5) Change-Id: I31c60521e98ea223bd1b66d3fe66f3212e1036a9 Signed-off-by: Alfredo Moralejo --- watcher/common/nova_helper.py | 35 +++-- watcher/tests/unit/common/test_nova_helper.py | 129 ++++++++++++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/watcher/common/nova_helper.py b/watcher/common/nova_helper.py index cd4481fc8..ed1dbb3ef 100644 --- a/watcher/common/nova_helper.py +++ b/watcher/common/nova_helper.py @@ -569,6 +569,28 @@ class NovaHelper: flavors = self.connection.compute.flavors(is_public=None) return [Flavor.from_openstacksdk(f) for f in flavors] + def get_flavor_id(self, flavor): + """Get the flavor id for a given flavor name or id. + + :param flavor: the name or id of the flavor to get + :returns: the flavor id if found + :raises: ComputeResourceNotFound if no flavor was found + :raises: NovaClientError if there is any problem while calling the + Nova api + """ + + try: + flavor_obj = self._get_flavor(flavor) + return flavor_obj.id + except exception.ComputeResourceNotFound: + flavor_id = next((f.id for f in self.get_flavor_list() if + f.flavor_name == flavor), None) + if flavor_id: + return flavor_id + + msg = f"{flavor} of type Flavor" + raise exception.ComputeResourceNotFound(msg) + @nova_retries @handle_nova_error("Flavor") def _get_flavor(self, flavor): @@ -873,18 +895,13 @@ class NovaHelper: flavor_id = None try: - try: - flavor_obj = self._get_flavor(flavor) - flavor_id = flavor_obj.id - except exception.ComputeResourceNotFound: - flavor_id = next((f.id for f in self.get_flavor_list() if - f.flavor_name == flavor), None) + flavor_id = self.get_flavor_id(flavor) + except exception.ComputeResourceNotFound: + LOG.debug("Flavor not found: %s, could not resize", flavor) + return False except exception.NovaClientError as e: LOG.debug("Nova client exception occurred while resizing " "instance %s. Exception: %s", instance_id, e) - - if not flavor_id: - LOG.debug("Flavor not found: %s, could not resize", flavor) return False instance_status = instance.vm_state diff --git a/watcher/tests/unit/common/test_nova_helper.py b/watcher/tests/unit/common/test_nova_helper.py index 058477ab9..314488d05 100644 --- a/watcher/tests/unit/common/test_nova_helper.py +++ b/watcher/tests/unit/common/test_nova_helper.py @@ -1308,6 +1308,135 @@ class TestNovaHelper(test_utils.NovaResourcesMixin, base.TestCase): # Verify that the result is cached self.assertIsNotNone(nova_util._is_pinned_az_available) + def test_get_flavor_id_by_id(self, mock_cinder): + """Test get_flavor_id returns id when flavor is found by ID.""" + nova_util = nova_helper.NovaHelper() + flavor_id = 'flavor-123' + flavor = self.create_openstacksdk_flavor( + id=flavor_id, name='m1.small' + ) + self.mock_connection.compute.get_flavor.return_value = flavor + + result = nova_util.get_flavor_id(flavor_id) + + self.assertEqual(flavor_id, result) + self.mock_connection.compute.get_flavor.assert_called_once_with( + flavor_id + ) + + def test_get_flavor_id_by_name(self, mock_cinder): + """Test get_flavor_id returns id when flavor is found by name.""" + nova_util = nova_helper.NovaHelper() + flavor_name = 'm1.small' + flavor_id = 'flavor-123' + flavor = self.create_openstacksdk_flavor( + id=flavor_id, name=flavor_name + ) + + # First attempt to get by ID fails (NotFoundException) + self.mock_connection.compute.get_flavor.side_effect = ( + sdk_exc.NotFoundException() + ) + # get_flavor_list returns list with the flavor + self.mock_connection.compute.flavors.return_value = [flavor] + + result = nova_util.get_flavor_id(flavor_name) + + self.assertEqual(flavor_id, result) + self.mock_connection.compute.get_flavor.assert_called_once_with( + flavor_name + ) + self.mock_connection.compute.flavors.assert_called_once_with( + is_public=None + ) + + def test_get_flavor_id_not_found_by_id_or_name(self, mock_cinder): + """Test get_flavor_id raises exception when flavor is not found.""" + nova_util = nova_helper.NovaHelper() + flavor_name = 'nonexistent-flavor' + + # First attempt to get by ID fails + self.mock_connection.compute.get_flavor.side_effect = ( + sdk_exc.NotFoundException() + ) + # get_flavor_list returns empty list + self.mock_connection.compute.flavors.return_value = [] + + self.assertRaisesRegex( + exception.ComputeResourceNotFound, + f"{flavor_name} of type Flavor", + nova_util.get_flavor_id, + flavor_name + ) + + def test_get_flavor_id_not_found_in_list(self, mock_cinder): + """Test get_flavor_id when flavor name not in returned list.""" + nova_util = nova_helper.NovaHelper() + flavor_name = 'm1.small' + + # First attempt to get by ID fails + self.mock_connection.compute.get_flavor.side_effect = ( + sdk_exc.NotFoundException() + ) + # get_flavor_list returns flavors but none match the name + other_flavor = self.create_openstacksdk_flavor( + id='other-id', name='m1.large' + ) + self.mock_connection.compute.flavors.return_value = [other_flavor] + + self.assertRaisesRegex( + exception.ComputeResourceNotFound, + f"{flavor_name} of type Flavor", + nova_util.get_flavor_id, + flavor_name + ) + + def test_get_flavor_id_sdk_exception(self, mock_cinder): + """Test get_flavor_id raises NovaClientError on SDK exception.""" + nova_util = nova_helper.NovaHelper() + flavor_id = 'flavor-123' + + # SDK raises a generic exception + self.mock_connection.compute.get_flavor.side_effect = ( + sdk_exc.SDKException("Connection error") + ) + + self.assertRaises( + exception.NovaClientError, + nova_util.get_flavor_id, + flavor_id + ) + + def test_get_flavor_id_by_name_multiple_flavors(self, mock_cinder): + """Test get_flavor_id finds correct flavor by name in list.""" + nova_util = nova_helper.NovaHelper() + flavor_name = 'm1.medium' + target_id = 'flavor-456' + + # Create multiple flavors + flavor1 = self.create_openstacksdk_flavor( + id='flavor-123', name='m1.small' + ) + flavor2 = self.create_openstacksdk_flavor( + id=target_id, name=flavor_name + ) + flavor3 = self.create_openstacksdk_flavor( + id='flavor-789', name='m1.large' + ) + + # First attempt to get by ID fails + self.mock_connection.compute.get_flavor.side_effect = ( + sdk_exc.NotFoundException() + ) + # get_flavor_list returns multiple flavors + self.mock_connection.compute.flavors.return_value = [ + flavor1, flavor2, flavor3 + ] + + result = nova_util.get_flavor_id(flavor_name) + + self.assertEqual(target_id, result) + class TestNovaRetries(base.TestCase): """Test suite for the nova_retries decorator."""