openstacksdk/openstack/connection.py

379 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 region of
a cloud provider. The :class:`~openstack.connection.Connection` has an
attribute to access each OpenStack service.
At a minimum, the :class:`~openstack.connection.Connection` class needs to be
created with a config or the parameters to build one.
While the overall system is very flexible, there are four main use cases
for different ways to create a :class:`~openstack.connection.Connection`.
* Using config settings and keyword arguments as described in
:ref:`openstack-config`
* Using only keyword arguments passed to the constructor ignoring config files
and environment variables.
* Using an existing authenticated `keystoneauth1.session.Session`, such as
might exist inside of an OpenStack service operational context.
* Using an existing :class:`~openstack.config.cloud_region.CloudRegion`.
Using config settings
---------------------
For users who want to create a :class:`~openstack.connection.Connection` making
use of named clouds in ``clouds.yaml`` files, ``OS_`` environment variables
and python keyword arguments, the :func:`openstack.connect` factory function
is the recommended way to go:
.. code-block:: python
import openstack
conn = openstack.connect(cloud='example', region_name='earth1')
If the application in question is a command line application that should also
accept command line arguments, an `argparse.Namespace` can be passed to
:func:`openstack.connect` that will have relevant arguments added to it and
then subsequently consumed by the constructor:
.. code-block:: python
import argparse
import openstack
options = argparse.ArgumentParser(description='Awesome OpenStack App')
conn = openstack.connect(options=options)
Using Only Keyword Arguments
----------------------------
If the application wants to avoid loading any settings from ``clouds.yaml`` or
environment variables, use the :class:`~openstack.connection.Connection`
constructor directly. As long as the ``cloud`` argument is omitted or ``None``,
the :class:`~openstack.connection.Connection` constructor will not load
settings from files or the environment.
.. note::
This is a different default behavior than the :func:`~openstack.connect`
factory function. In :func:`~openstack.connect` if ``cloud`` is omitted
or ``None``, a default cloud will be loaded, defaulting to the ``envvars``
cloud if it exists.
.. code-block:: python
from openstack import connection
conn = connection.Connection(
region_name='example-region',
auth=dict(
auth_url='https://auth.example.com',
username='amazing-user',
password='super-secret-password',
project_id='33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5',
user_domain_id='054abd68-9ad9-418b-96d3-3437bb376703'),
compute_api_version='2',
identity_interface='internal')
Per-service settings as needed by `keystoneauth1.adapter.Adapter` such as
``api_version``, ``service_name``, and ``interface`` can be set, as seen
above, by prefixing them with the official ``service-type`` name of the
service. ``region_name`` is a setting for the entire
:class:`~openstack.config.cloud_region.CloudRegion` and cannot be set per
service.
From existing authenticated Session
-----------------------------------
For applications that already have an authenticated Session, simply passing
it to the :class:`~openstack.connection.Connection` constructor is all that
is needed:
.. code-block:: python
from openstack import connection
conn = connection.Connection(
session=session,
region_name='example-region',
compute_api_version='2',
identity_interface='internal')
From existing CloudRegion
-------------------------
If you already have an :class:`~openstack.config.cloud_region.CloudRegion`
you can pass it in instead:
.. code-block:: python
from openstack import connection
import openstack.config
config = openstack.config.get_cloud_region(
cloud='example', region_name='earth')
conn = connection.Connection(config=config)
Using the Connection
--------------------
Services are accessed through an attribute named after the service's official
service-type.
List
~~~~
An iterator containing a list of all the projects is retrieved in this manner:
.. code-block:: python
projects = conn.identity.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")
Additional information about the services can be found in the
:ref:`service-proxies` documentation.
"""
import warnings
import keystoneauth1.exceptions
import requestsexceptions
import six
from openstack import _log
from openstack._meta import connection as _meta
from openstack.cloud import openstackcloud as _cloud
from openstack import config as _config
from openstack.config import cloud_region
from openstack import exceptions
from openstack import service_description
__all__ = [
'from_config',
'Connection',
]
if requestsexceptions.SubjectAltNameWarning:
warnings.filterwarnings(
'ignore', category=requestsexceptions.SubjectAltNameWarning)
_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
`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 = kwargs.pop('cloud_name', cloud)
config = kwargs.pop('cloud_config', config)
if config is None:
config = _config.OpenStackConfig().get_one(
cloud=cloud, argparse=options, **kwargs)
return Connection(config=config)
class Connection(six.with_metaclass(_meta.ConnectionMeta,
_cloud._OpenStackCloudMixin)):
def __init__(self, cloud=None, config=None, session=None,
app_name=None, app_version=None,
extra_services=None,
strict=False,
use_direct_get=False,
task_manager=None,
rate_limit=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 extra_services: List of
:class:`~openstack.service_description.ServiceDescription`
objects describing services that openstacksdk otherwise does not
know about.
:param bool use_direct_get:
For get methods, make specific REST calls for server-side
filtering instead of making list calls and filtering client-side.
Default false.
:param task_manager:
Ignored. Exists for backwards compat during transition. Rate limit
parameters should be passed directly to the `rate_limit` parameter.
:param rate_limit:
Client-side rate limit, expressed in calls per second. The
parameter can either be a single float, or it can be a dict with
keys as service-type and values as floats expressing the calls
per second for that service. Defaults to None, which means no
rate-limiting is performed.
: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 constructor.
"""
self.config = config
self._extra_services = {}
if extra_services:
for service in extra_services:
self._extra_services[service.service_type] = service
if not self.config:
if session:
self.config = cloud_region.from_session(
session=session,
app_name=app_name, app_version=app_version,
load_yaml_config=False,
load_envvars=False,
rate_limit=rate_limit,
**kwargs)
else:
self.config = _config.get_cloud_region(
cloud=cloud,
app_name=app_name, app_version=app_version,
load_yaml_config=cloud is not None,
load_envvars=cloud is not None,
rate_limit=rate_limit,
**kwargs)
self._session = None
self._proxies = {}
self.use_direct_get = use_direct_get
self.strict_mode = strict
# Call the _OpenStackCloudMixin constructor while we work on
# integrating things better.
_cloud._OpenStackCloudMixin.__init__(self)
@property
def session(self):
if not self._session:
self._session = self.config.get_session()
# Hide a reference to the connection on the session to help with
# backwards compatibility for folks trying to just pass
# conn.session to a Resource method's session argument.
self.session._sdk_connection = self
return self._session
def add_service(self, service):
"""Add a service to the Connection.
Attaches an instance of the :class:`~openstack.proxy.Proxy`
class contained in
:class:`~openstack.service_description.ServiceDescription`.
The :class:`~openstack.proxy.Proxy` will be attached to the
`Connection` by its ``service_type`` and by any ``aliases`` that
may be specified.
:param openstack.service_description.ServiceDescription service:
Object describing the service to be attached. As a convenience,
if ``service`` is a string it will be treated as a ``service_type``
and a basic
:class:`~openstack.service_description.ServiceDescription`
will be created.
"""
# If we don't have a proxy, just instantiate Proxy so that
# we get an adapter.
if isinstance(service, six.string_types):
service = service_description.ServiceDescription(service)
# Directly invoke descriptor of the ServiceDescription
def getter(self):
return service.__get__(self, service)
# Register the ServiceDescription class (as property)
# with every known alias for a "runtime descriptor"
for attr_name in service.all_types:
setattr(
self.__class__,
attr_name.replace('-', '_'),
property(fget=getter)
)
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)
def close(self):
"""Release any resources held open."""
if self.__pool_executor:
self.__pool_executor.shutdown()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()