diff --git a/CHANGES.rst b/CHANGES.rst index 5b208a8130..0359f7fc58 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,13 @@ Common Compute ~~~~~~~ +- [OpenStack] Select service catalog endpoints by service type without filtering + by the default service name. The service name is only used as a filter when + explicitly provided via ``ex_force_service_name``. + + (#2074) + [Miguel Caballer - @micafer] + - [Compute] Unify ``NodeDriver`` method signatures across drivers. Driver implementations now preserve the standard argument order and diff --git a/libcloud/common/openstack.py b/libcloud/common/openstack.py index ad3ada1756..c361972f34 100644 --- a/libcloud/common/openstack.py +++ b/libcloud/common/openstack.py @@ -308,7 +308,7 @@ def get_endpoint(self): :returns: url of the relevant endpoint for the driver """ service_type = self.service_type - service_name = self.service_name + service_name = None service_region = self.service_region if self._ex_force_service_type: @@ -318,9 +318,23 @@ def get_endpoint(self): if self._ex_force_service_region: service_region = self._ex_force_service_region - endpoint = self.service_catalog.get_endpoint( - service_type=service_type, name=service_name, region=service_region - ) + try: + endpoint = self.service_catalog.get_endpoint( + service_type=service_type, name=service_name, region=service_region + ) + except ValueError: + # An explicitly provided service name is a strict filter. When no + # name was provided, first allow the service type and region to + # identify the endpoint on their own, and only use the driver's + # default service name to resolve an ambiguous result. + if service_name or not self.service_name: + raise + + endpoint = self.service_catalog.get_endpoint( + service_type=service_type, + name=self.service_name, + region=service_region, + ) url = endpoint.url diff --git a/libcloud/test/common/test_openstack.py b/libcloud/test/common/test_openstack.py index 2bfeb76401..18d6eef28c 100644 --- a/libcloud/test/common/test_openstack.py +++ b/libcloud/test/common/test_openstack.py @@ -57,6 +57,78 @@ def test_set_microversion(self): headers = self.connection.add_default_headers({}) self.assertEqual(headers["OpenStack-API-Version"], "volume 2.67") + def test_get_endpoint_does_not_filter_by_default_service_name(self): + self.connection.service_catalog = Mock() + self.connection.service_catalog.get_endpoint.return_value.url = ( + "https://compute.example.com" + ) + self.connection.service_type = "compute" + self.connection.service_name = "nova" + self.connection.service_region = "RegionOne" + + endpoint = self.connection.get_endpoint() + + self.assertEqual(endpoint, "https://compute.example.com") + self.connection.service_catalog.get_endpoint.assert_called_once_with( + service_type="compute", name=None, region="RegionOne" + ) + + def test_get_endpoint_filters_by_explicit_service_name(self): + self.connection.service_catalog = Mock() + self.connection.service_catalog.get_endpoint.return_value.url = ( + "https://compute.example.com" + ) + self.connection.service_type = "compute" + self.connection.service_name = "nova" + self.connection.service_region = "RegionOne" + self.connection._ex_force_service_name = "custom-nova" + + endpoint = self.connection.get_endpoint() + + self.assertEqual(endpoint, "https://compute.example.com") + self.connection.service_catalog.get_endpoint.assert_called_once_with( + service_type="compute", name="custom-nova", region="RegionOne" + ) + + def test_get_endpoint_uses_default_service_name_to_resolve_ambiguity(self): + self.connection.service_catalog = Mock() + endpoint = Mock(url="https://compute.example.com") + self.connection.service_catalog.get_endpoint.side_effect = [ + ValueError("Found more than 1 matching endpoint"), + endpoint, + ] + self.connection.service_type = "compute" + self.connection.service_name = "nova" + self.connection.service_region = "RegionOne" + + result = self.connection.get_endpoint() + + self.assertEqual(result, "https://compute.example.com") + self.assertEqual( + self.connection.service_catalog.get_endpoint.call_args_list, + [ + unittest.mock.call(service_type="compute", name=None, region="RegionOne"), + unittest.mock.call(service_type="compute", name="nova", region="RegionOne"), + ], + ) + + def test_get_endpoint_does_not_fallback_from_explicit_service_name(self): + self.connection.service_catalog = Mock() + self.connection.service_catalog.get_endpoint.side_effect = ValueError( + "Found more than 1 matching endpoint" + ) + self.connection.service_type = "compute" + self.connection.service_name = "nova" + self.connection.service_region = "RegionOne" + self.connection._ex_force_service_name = "custom-nova" + + with self.assertRaisesRegex(ValueError, "more than 1"): + self.connection.get_endpoint() + + self.connection.service_catalog.get_endpoint.assert_called_once_with( + service_type="compute", name="custom-nova", region="RegionOne" + ) + @patch("libcloud.common.base.ConnectionUserAndKey.request") def test_request(self, mock_request): OpenStackBaseConnection.conn_class._raw_data = ""