# Copyright 2018 Red Hat, Inc. # 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 warnings import os_service_types from openstack import _log from openstack import exceptions __all__ = [ 'ServiceDescription', ] _logger = _log.setup_logging('openstack') _service_type_manager = os_service_types.ServiceTypes() class ServiceDescription(object): #: Dictionary of supported versions and proxy classes for that version supported_versions = None #: main service_type to use to find this service in the catalog service_type = None #: list of aliases this service might be registered as aliases = [] def __init__(self, service_type, supported_versions=None, aliases=None): """Class describing how to interact with a REST service. Each service in an OpenStack cloud needs to be found by looking for it in the catalog. Once the endpoint is found, REST calls can be made, but a Proxy class and some Resource objects are needed to provide an object interface. Instances of ServiceDescription can be passed to `openstack.connection.Connection.add_service`, or a list can be passed to the `openstack.connection.Connection` constructor in the ``extra_services`` argument. All three parameters can be provided at instantation time, or a service-specific subclass can be used that sets the attributes directly. :param string service_type: service_type to look for in the keystone catalog :param list aliases: Optional list of aliases, if there is more than one name that might be used to register the service in the catalog. """ self.service_type = service_type or self.service_type self.supported_versions = ( supported_versions or self.supported_versions or {}) self.aliases = aliases or self.aliases self.all_types = [service_type] + self.aliases def __get__(self, instance, owner): if instance is None: return self if self.service_type not in instance._proxies: instance._proxies[self.service_type] = self._make_proxy(instance) instance._proxies[self.service_type]._connection = instance return instance._proxies[self.service_type] def _make_proxy(self, instance): """Create a Proxy for the service in question. :param instance: The `openstack.connection.Connection` we're working with. """ config = instance.config # First, check to see if we've got config that matches what we # understand in the SDK. version_string = config.get_api_version(self.service_type) endpoint_override = config.get_endpoint(self.service_type) # If the user doesn't give a version in config, but we only support # one version, then just use that version. if not version_string and len(self.supported_versions) == 1: version_string = list(self.supported_versions)[0] proxy_obj = None if endpoint_override and version_string and self.supported_versions: # Both endpoint override and version_string are set, we don't # need to do discovery - just trust the user. proxy_class = self.supported_versions.get(version_string[0]) if proxy_class: proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, ) else: warnings.warn( "The configured version, {version} for service" " {service_type} is not known or supported by" " openstacksdk. The resulting Proxy object will only" " have direct passthrough REST capabilities.".format( version=version_string, service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) elif endpoint_override and self.supported_versions: temp_adapter = config.get_session_client( self.service_type ) api_version = temp_adapter.get_endpoint_data().api_version proxy_class = self.supported_versions.get(str(api_version[0])) if proxy_class: proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, ) else: warnings.warn( "Service {service_type} has an endpoint override set" " but the version discovered at that endpoint, {version}" " is not supported by openstacksdk. The resulting Proxy" " object will only have direct passthrough REST" " capabilities.".format( version=api_version, service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) if proxy_obj: if getattr(proxy_obj, 'skip_discovery', False): # Some services, like swift, don't have discovery. While # keystoneauth will behave correctly and handle such # scenarios, it's not super efficient as it involves trying # and falling back a few times. return proxy_obj data = proxy_obj.get_endpoint_data() # If we've gotten here with a proxy object it means we have # an endpoint_override in place. If the catalog_url and # service_url don't match, which can happen if there is a # None plugin and auth.endpoint like with standalone ironic, # we need to be explicit that this service has an endpoint_override # so that subsequent discovery calls don't get made incorrectly. if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( service_type=self.service_type) config.config[ep_key] = data.service_url proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, ) return proxy_obj # Make an adapter to let discovery take over version_kwargs = {} if version_string: version_kwargs['version'] = version_string elif self.supported_versions: supported_versions = sorted([ int(f) for f in self.supported_versions]) version_kwargs['min_version'] = str(supported_versions[0]) version_kwargs['max_version'] = '{version}.latest'.format( version=str(supported_versions[-1])) temp_adapter = config.get_session_client( self.service_type, allow_version_hack=True, **version_kwargs ) found_version = temp_adapter.get_api_major_version() if found_version is None: # Maybe openstacksdk is being used for the passthrough # REST API proxy layer for an unknown service in the # service catalog that also doesn't have any useful # version discovery? warnings.warn( "Service {service_type} has no discoverable version." " The resulting Proxy object will only have direct" " passthrough REST capabilities.".format( service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) return temp_adapter proxy_class = self.supported_versions.get(str(found_version[0])) if proxy_class: version_kwargs['constructor'] = proxy_class return config.get_session_client( self.service_type, allow_version_hack=True, **version_kwargs ) def __set__(self, instance, value): raise AttributeError('Service Descriptors cannot be set') def __delete__(self, instance): raise AttributeError('Service Descriptors cannot be deleted')