diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 96b6c80a4..0a5d36eb5 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -73,7 +73,12 @@ OpenStack services. connection Once you have a *Connection* instance, the following services may be exposed -to you. The combination of your ``CloudRegion`` and the catalog of the cloud +to you via the :class:`~openstack.proxy.BaseProxy` interface. + +.. autoclass:: openstack.proxy.BaseProxy + :members: + +The combination of your ``CloudRegion`` and the catalog of the cloud in question control which services are exposed, but listed below are the ones provided by the SDK. diff --git a/openstack/_meta.py b/openstack/_meta.py new file mode 100644 index 000000000..da0bf6c83 --- /dev/null +++ b/openstack/_meta.py @@ -0,0 +1,126 @@ +# 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 importlib +import warnings + +import os_service_types + +from openstack import _log +from openstack import proxy +from openstack import service_description + +_logger = _log.setup_logging('openstack') +_service_type_manager = os_service_types.ServiceTypes() +_DOC_TEMPLATE = ( + ":class:`{class_name}` for {service_type} aka {project}") +_PROXY_TEMPLATE = """Proxy for {service_type} aka {project} + +This proxy object could be an instance of +{class_doc_strings} +depending on client configuration and which version of the service is +found on remotely on the cloud. +""" + + +class ConnectionMeta(type): + def __new__(meta, name, bases, dct): + for service in _service_type_manager.services: + service_type = service['service_type'] + if service_type == 'ec2-api': + # NOTE(mordred) It doesn't make any sense to use ec2-api + # from openstacksdk. The credentials API calls are all calls + # on identity endpoints. + continue + desc_class = service_description.ServiceDescription + service_filter_class = _find_service_filter_class(service_type) + descriptor_args = {'service_type': service_type} + if service_filter_class: + desc_class = service_description.OpenStackServiceDescription + descriptor_args['service_filter_class'] = service_filter_class + class_names = service_filter_class._get_proxy_class_names() + if len(class_names) == 1: + doc = _DOC_TEMPLATE.format( + class_name="{service_type} Proxy <{name}>".format( + service_type=service_type, name=class_names[0]), + **service) + else: + class_doc_strings = "\n".join([ + ":class:`{class_name}`".format(class_name=class_name) + for class_name in class_names]) + doc = _PROXY_TEMPLATE.format( + class_doc_strings=class_doc_strings, **service) + else: + descriptor_args['proxy_class'] = proxy.BaseProxy + doc = _DOC_TEMPLATE.format( + class_name='~openstack.proxy.BaseProxy', **service) + descriptor = desc_class(**descriptor_args) + descriptor.__doc__ = doc + dct[service_type.replace('-', '_')] = descriptor + + # Register the descriptor class with every known alias. Don't + # add doc strings though - although they are supported, we don't + # want to give anybody any bad ideas. Making a second descriptor + # does not introduce runtime cost as the descriptors all use + # the same _proxies dict on the instance. + for alias_name in _get_aliases(service_type): + if alias_name[-1].isdigit(): + continue + alias_descriptor = desc_class(**descriptor_args) + dct[alias_name.replace('-', '_')] = alias_descriptor + return super(ConnectionMeta, meta).__new__(meta, name, bases, dct) + + +def _get_aliases(service_type, aliases=None): + # We make connection attributes for all official real type names + # and aliases. Three services have names they were called by in + # openstacksdk that are not covered by Service Types Authority aliases. + # Include them here - but take heed, no additional values should ever + # be added to this list. + # that were only used in openstacksdk resource naming. + LOCAL_ALIASES = { + 'baremetal': 'bare_metal', + 'block_storage': 'block_store', + 'clustering': 'cluster', + } + all_types = set(_service_type_manager.get_aliases(service_type)) + if aliases: + all_types.update(aliases) + if service_type in LOCAL_ALIASES: + all_types.add(LOCAL_ALIASES[service_type]) + return all_types + + +def _find_service_filter_class(service_type): + package_name = 'openstack.{service_type}'.format( + service_type=service_type).replace('-', '_') + module_name = service_type.replace('-', '_') + '_service' + class_name = ''.join( + [part.capitalize() for part in module_name.split('_')]) + try: + import_name = '.'.join([package_name, module_name]) + service_filter_module = importlib.import_module(import_name) + except ImportError as e: + # ImportWarning is ignored by default. This warning is here + # as an opt-in for people trying to figure out why something + # didn't work. + warnings.warn( + "Could not import {service_type} service filter: {e}".format( + service_type=service_type, e=str(e)), + ImportWarning) + return None + # There are no cases in which we should have a module but not the class + # inside it. + service_filter_class = getattr(service_filter_module, class_name) + return service_filter_class diff --git a/openstack/connection.py b/openstack/connection.py index 763621c09..5f454480f 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -82,11 +82,11 @@ __all__ = [ import warnings import keystoneauth1.exceptions -import os_service_types import requestsexceptions import six from openstack import _log +from openstack import _meta from openstack import config as _config from openstack import exceptions from openstack import service_description @@ -128,7 +128,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): return Connection(config=config) -class Connection(object): +class Connection(six.with_metaclass(_meta.ConnectionMeta)): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, @@ -219,11 +219,7 @@ class Connection(object): # to a Resource method's session argument. self.session._sdk_connection = self - service_type_manager = os_service_types.ServiceTypes() - for service in service_type_manager.services: - self.add_service( - service_description.OpenStackServiceDescription( - service, self.config)) + self._proxies = {} def add_service(self, service): """Add a service to the Connection. @@ -245,29 +241,16 @@ class Connection(object): # If we don't have a proxy, just instantiate BaseProxy so that # we get an adapter. if isinstance(service, six.string_types): - service_type = service - service = service_description.ServiceDescription(service_type) - else: - service_type = service.service_type - proxy_object = service.proxy_class( - session=self.config.get_session(), - task_manager=self.task_manager, - allow_version_hack=True, - service_type=self.config.get_service_type(service_type), - service_name=self.config.get_service_name(service_type), - interface=self.config.get_interface(service_type), - region_name=self.config.region_name, - version=self.config.get_api_version(service_type), - ) + service = service_description.ServiceDescription(service) # Register the proxy class with every known alias for attr_name in service.all_types: - setattr(self, attr_name.replace('-', '_'), proxy_object) + setattr(self, attr_name.replace('-', '_'), service) def authorize(self): """Authorize this Connection - **NOTE**: This method is optional. When an application makes a call + .. note:: This method is optional. When an application makes a call to any OpenStack service, this method allows you to request a token manually before attempting to do anything else. diff --git a/openstack/proxy.py b/openstack/proxy.py index 6954c823a..4ad09909a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -41,6 +41,7 @@ def _check_resource(strict=False): class BaseProxy(_adapter.OpenStackSDKAdapter): + """Represents a service.""" def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on diff --git a/openstack/service_description.py b/openstack/service_description.py index c141318e9..ef3647bd1 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -17,7 +17,6 @@ __all__ = [ ] import importlib -import warnings import os_service_types @@ -28,26 +27,6 @@ _logger = _log.setup_logging('openstack') _service_type_manager = os_service_types.ServiceTypes() -def _get_all_types(service_type, aliases=None): - # We make connection attributes for all official real type names - # and aliases. Three services have names they were called by in - # openstacksdk that are not covered by Service Types Authority aliases. - # Include them here - but take heed, no additional values should ever - # be added to this list. - # that were only used in openstacksdk resource naming. - LOCAL_ALIASES = { - 'baremetal': 'bare_metal', - 'block_storage': 'block_store', - 'clustering': 'cluster', - } - all_types = set(_service_type_manager.get_all_types(service_type)) - if aliases: - all_types.update(aliases) - if service_type in LOCAL_ALIASES: - all_types.add(LOCAL_ALIASES[service_type]) - return all_types - - class ServiceDescription(object): #: Proxy class for this service @@ -85,11 +64,13 @@ class ServiceDescription(object): 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 + self.service_type = service_type or self.service_type self.proxy_class = proxy_class or self.proxy_class - self.all_types = _get_all_types(service_type, aliases) - - self._validate_proxy_class() + if self.proxy_class: + self._validate_proxy_class() + self.aliases = aliases or self.aliases + self.all_types = [service_type] + self.aliases + self._proxy = None def _validate_proxy_class(self): if not issubclass(self.proxy_class, proxy.BaseProxy): @@ -98,10 +79,36 @@ class ServiceDescription(object): module=self.proxy_class.__module__, proxy_class=self.proxy_class.__name__)) + def get_proxy_class(self, config): + return self.proxy_class + + def __get__(self, instance, owner): + if instance is None: + return self + if self.service_type not in instance._proxies: + config = instance.config + proxy_class = self.get_proxy_class(config) + instance._proxies[self.service_type] = proxy_class( + session=instance.config.get_session(), + task_manager=instance.task_manager, + allow_version_hack=True, + service_type=config.get_service_type(self.service_type), + service_name=config.get_service_name(self.service_type), + interface=config.get_interface(self.service_type), + region_name=config.region_name, + version=config.get_api_version(self.service_type) + ) + return instance._proxies[self.service_type] + + def __set__(self, instance, value): + raise AttributeError('Service Descriptors cannot be set') + + def __delete__(self, instance): + raise AttributeError('Service Descriptors cannot be deleted') + class OpenStackServiceDescription(ServiceDescription): - - def __init__(self, service, config): + def __init__(self, service_filter_class, *args, **kwargs): """Official OpenStack ServiceDescription. The OpenStackServiceDescription class is a helper class for @@ -111,57 +118,19 @@ class OpenStackServiceDescription(ServiceDescription): It finds the proxy_class by looking in the openstacksdk tree for appropriately named modules. - :param dict service: - A service dict as found in `os_service_types.ServiceTypes.services` - :param openstack.config.cloud_region.CloudRegion config: - ConfigRegion for the connection. + :param service_filter_class: + A subclass of :class:`~openstack.service_filter.ServiceFilter` """ - super(OpenStackServiceDescription, self).__init__( - service['service_type']) - self.config = config - service_filter = self._get_service_filter() - if service_filter: - module_name = service_filter.get_module() + "._proxy" - module = importlib.import_module(module_name) - self.proxy_class = getattr(module, "Proxy") + super(OpenStackServiceDescription, self).__init__(*args, **kwargs) + self._service_filter_class = service_filter_class - def _get_service_filter(self): - service_filter_class = None - for service_type in self.all_types: - service_filter_class = self._find_service_filter_class() - if service_filter_class: - break - if not service_filter_class: - return None + def get_proxy_class(self, config): # TODO(mordred) Replace this with proper discovery - version_string = self.config.get_api_version(self.service_type) + version_string = config.get_api_version(self.service_type) version = None if version_string: version = 'v{version}'.format(version=version_string[0]) - return service_filter_class(version=version) - - def _find_service_filter_class(self): - package_name = 'openstack.{service_type}'.format( - service_type=self.service_type).replace('-', '_') - module_name = self.service_type.replace('-', '_') + '_service' - class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) - try: - import_name = '.'.join([package_name, module_name]) - service_filter_module = importlib.import_module(import_name) - except ImportError as e: - # ImportWarning is ignored by default. This warning is here - # as an opt-in for people trying to figure out why something - # didn't work. - warnings.warn( - "Could not import {service_type} service filter: {e}".format( - service_type=self.service_type, e=str(e)), - ImportWarning) - return None - service_filter_class = getattr(service_filter_module, class_name, None) - if not service_filter_class: - _logger.warn( - 'Unable to find class %s in module for service %s', - class_name, self.service_type) - return None - return service_filter_class + service_filter = self._service_filter_class(version=version) + module_name = service_filter.get_module() + "._proxy" + module = importlib.import_module(module_name) + return getattr(module, "Proxy") diff --git a/openstack/service_filter.py b/openstack/service_filter.py index a0f60e3b6..b5c4fe82d 100644 --- a/openstack/service_filter.py +++ b/openstack/service_filter.py @@ -92,6 +92,16 @@ class ServiceFilter(dict): self['api_version'] = api_version self['requires_project_id'] = requires_project_id + @classmethod + def _get_proxy_class_names(cls): + names = [] + module_name = ".".join(cls.__module__.split('.')[:-1]) + for version in cls.valid_versions: + names.append("{module}.{version}._proxy.Proxy".format( + module=module_name, + version=version.module)) + return names + @property def service_type(self): return self['service_type']