openstacksdk/openstack/connection.py

335 lines
14 KiB
Python

# 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.
"""
The :class:`~openstack.connection.Connection` class is the primary interface
to the Python SDK it maintains a context for a connection to a cloud provider.
The connection has an attribute to access each supported service.
Examples
--------
At a minimum, the :class:`~openstack.connection.Connection` class needs to be
created with a config or the parameters to build one.
Create a connection
~~~~~~~~~~~~~~~~~~~
The preferred way to create a connection is to manage named configuration
settings in your clouds.yaml file and refer to them by name.::
from openstack import connection
conn = connection.Connection(cloud='example', region_name='earth1')
If you already have an :class:`~openstack.config.cloud_region.CloudRegion`
you can pass it in instead.::
from openstack import connection
import openstack.config
config = openstack.config.OpenStackConfig.get_one(
cloud='example', region_name='earth')
conn = connection.Connection(config=config)
It's also possible to pass in parameters directly if needed. The following
example constructor uses the default identity password auth
plugin and provides a username and password.::
from openstack import connection
auth_args = {
'auth_url': 'http://172.20.1.108:5000/v3',
'project_name': 'admin',
'user_domain_name': 'default',
'project_domain_name': 'default',
'username': 'admin',
'password': 'admin',
}
conn = connection.Connection(**auth_args)
List
~~~~
Services are accessed through an attribute named after the service's official
service-type. A list of all the projects is retrieved in this manner::
projects = conn.identity.list_projects()
Find or create
~~~~~~~~~~~~~~
If you wanted to make sure you had a network named 'zuul', you would first
try to find it and if that fails, you would create it::
network = conn.network.find_network("zuul")
if network is None:
network = conn.network.create_network({"name": "zuul"})
"""
import importlib
import keystoneauth1.exceptions
import os_service_types
from six.moves import urllib
from openstack import _log
import openstack.config
from openstack.config import cloud_region
from openstack import exceptions
from openstack import proxy
from openstack import proxy2
from openstack import task_manager
_logger = _log.setup_logging('openstack')
def from_config(cloud=None, config=None, options=None, **kwargs):
"""Create a Connection using openstack.config
:param str cloud:
Use the `cloud` configuration details when creating the Connection.
:param openstack.config.cloud_region.CloudRegion config:
An existing CloudRegion configuration. If no `config` is provided,
`openstack.config.OpenStackConfig` will be called, and the provided
`name` will be used in determining which cloud's configuration
details will be used in creation of the `Connection` instance.
:param argparse.Namespace options:
Allows direct passing in of options to be added to the cloud config.
This does not have to be an actual instance of argparse.Namespace,
despite the naming of the the
`openstack.config.loader.OpenStackConfig.get_one` argument to which
it is passed.
:rtype: :class:`~openstack.connection.Connection`
"""
# TODO(mordred) Backwards compat while we transition
cloud = cloud or kwargs.get('cloud_name')
config = config or kwargs.get('cloud_config')
if config is None:
config = openstack.config.OpenStackConfig().get_one(
cloud=cloud, argparse=options)
return Connection(config=config)
class Connection(object):
def __init__(self, cloud=None, config=None, session=None,
app_name=None, app_version=None,
# TODO(shade) Remove these once we've shifted
# python-openstackclient to not use the profile interface.
authenticator=None, profile=None,
**kwargs):
"""Create a connection to a cloud.
A connection needs information about how to connect, how to
authenticate and how to select the appropriate services to use.
The recommended way to provide this information is by referencing
a named cloud config from an existing `clouds.yaml` file. The cloud
name ``envvars`` may be used to consume a cloud configured via ``OS_``
environment variables.
A pre-existing :class:`~openstack.config.cloud_region.CloudRegion`
object can be passed in lieu of a cloud name, for cases where the user
already has a fully formed CloudRegion and just wants to use it.
Similarly, if for some reason the user already has a
:class:`~keystoneauth1.session.Session` and wants to use it, it may be
passed in.
:param str cloud: Name of the cloud from config to use.
:param config: CloudRegion object representing the config for the
region of the cloud in question.
:type config: :class:`~openstack.config.cloud_region.CloudRegion`
:param session: A session object compatible with
:class:`~keystoneauth1.session.Session`.
:type session: :class:`~keystoneauth1.session.Session`
:param str app_name: Name of the application to be added to User Agent.
:param str app_version: Version of the application to be added to
User Agent.
:param authenticator: DEPRECATED. Only exists for short-term backwards
compatibility for python-openstackclient while we
transition.
:param profile: DEPRECATED. Only exists for short-term backwards
compatibility for python-openstackclient while we
transition.
:param kwargs: If a config is not provided, the rest of the parameters
provided are assumed to be arguments to be passed to the
CloudRegion contructor.
"""
self.config = config
self.service_type_manager = os_service_types.ServiceTypes()
if not self.config:
if profile:
# TODO(shade) Remove this once we've shifted
# python-openstackclient to not use the profile interface.
self.config = self._get_config_from_profile(
profile, authenticator, **kwargs)
else:
openstack_config = openstack.config.OpenStackConfig(
app_name=app_name, app_version=app_version,
load_yaml_config=profile is None)
self.config = openstack_config.get_one(
cloud=cloud, validate=session is None, **kwargs)
self.task_manager = task_manager.TaskManager(
name=':'.join([
self.config.name,
self.config.region_name or 'unknown']))
if session:
# TODO(mordred) Expose constructor option for this in OCC
self.config._keystone_session = session
self.session = self.config.get_session()
self._open()
def _get_config_from_profile(self, profile, authenticator, **kwargs):
"""Get openstack.config objects from legacy profile."""
# TODO(shade) Remove this once we've shifted python-openstackclient
# to not use the profile interface.
# We don't have a cloud name. Make one up from the auth_url hostname
# so that log messages work.
name = urllib.parse.urlparse(authenticator.auth_url).hostname
region_name = None
for service in profile.get_services():
if service.region:
region_name = service.region
service_type = service.service_type
if service.interface:
key = cloud_region._make_key('interface', service_type)
kwargs[key] = service.interface
if service.version:
version = service.version
if version.startswith('v'):
version = version[1:]
key = cloud_region._make_key('api_version', service_type)
kwargs[key] = service.version
config = cloud_region.CloudRegion(
name=name, region_name=region_name, config=kwargs)
config._auth = authenticator
def _open(self):
"""Open the connection. """
for service in self.service_type_manager.services:
self._load(service['service_type'])
# TODO(mordred) openstacksdk has support for the metric service
# which is not in service-types-authority. What do we do about that?
self._load('metric')
def _load(self, service_type):
service = self._get_service(service_type)
if service:
module_name = service.get_module() + "._proxy"
module = importlib.import_module(module_name)
proxy_class = getattr(module, "Proxy")
if not (issubclass(proxy_class, proxy.BaseProxy) or
issubclass(proxy_class, proxy2.BaseProxy)):
raise TypeError("%s.Proxy must inherit from BaseProxy" %
proxy_class.__module__)
else:
# If we don't have a proxy, just instantiate BaseProxy so that
# we get an adapter.
proxy_class = proxy2.BaseProxy
proxy_object = 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)
)
all_types = self.service_type_manager.get_all_types(service_type)
# Register the proxy class with every known alias
for attr_name in [name.replace('-', '_') for name in all_types]:
setattr(self, attr_name, proxy_object)
def _get_all_types(self, service_type):
# 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 = self.service_type_manager.get_all_types(service_type)
if service_type in LOCAL_ALIASES:
all_types.append(LOCAL_ALIASES[service_type])
return all_types
def _get_service(self, official_service_type):
service_class = None
for service_type in self._get_all_types(official_service_type):
service_class = self._find_service_class(service_type)
if service_class:
break
if not service_class:
return None
# TODO(mordred) Replace this with proper discovery
version_string = self.config.get_api_version(official_service_type)
version = None
if version_string:
version = 'v{version}'.format(version=version_string[0])
return service_class(version=version)
def _find_service_class(self, 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_module = importlib.import_module(import_name)
except ImportError:
return None
service_class = getattr(service_module, class_name, None)
if not service_class:
_logger.warn(
'Unable to find class {class_name} in module for service'
' for service {service_type}'.format(
class_name=class_name,
service_type=service_type))
return None
return service_class
def authorize(self):
"""Authorize this Connection
**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.
:returns: A string token.
:raises: :class:`~openstack.exceptions.HttpException` if the
authorization fails due to reasons like the credentials
provided are unable to be authorized or the `auth_type`
argument is missing, etc.
"""
try:
return self.session.get_token()
except keystoneauth1.exceptions.ClientException as e:
raise exceptions.raise_from_response(e.response)