2013-08-27 21:57:30 +00:00
|
|
|
# Copyright 2012-2013 OpenStack Foundation
|
2012-05-10 19:58:16 +00:00
|
|
|
#
|
2013-01-24 18:00:30 +00:00
|
|
|
# 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
|
2012-05-10 19:58:16 +00:00
|
|
|
#
|
2013-01-24 18:00:30 +00:00
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
2012-05-10 19:58:16 +00:00
|
|
|
#
|
2013-01-24 18:00:30 +00:00
|
|
|
# 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.
|
2012-05-10 19:58:16 +00:00
|
|
|
#
|
|
|
|
|
2013-01-31 19:31:41 +00:00
|
|
|
"""Manage access to the clients, including authenticating when needed."""
|
2012-05-02 21:02:08 +00:00
|
|
|
|
2015-07-19 18:15:04 +00:00
|
|
|
import copy
|
2012-05-02 21:02:08 +00:00
|
|
|
import logging
|
2013-11-21 00:02:09 +00:00
|
|
|
import sys
|
2012-05-02 21:02:08 +00:00
|
|
|
|
2015-10-07 03:42:40 +00:00
|
|
|
from oslo_utils import strutils
|
Propagate AttributeErrors when lazily loading plugins
Previously, if an AttributeError was raised in a plugin's make_client
method, the plugin simply wouldn't be an attribute of the ClientManager,
producing tracebacks like
Traceback (most recent call last):
File ".../openstackclient/shell.py", line 118, in run
ret_val = super(OpenStackShell, self).run(argv)
...
File ".../openstackclient/object/v1/container.py", line 150, in take_action
data = self.app.client_manager.object_store.container_list(
File ".../openstackclient/common/clientmanager.py", line 66, in __getattr__
raise AttributeError(name)
AttributeError: object_store
This made writing minimal third-party auth plugins difficult, as it
obliterated the original AttributeError.
Now, AttributeErrors that are raised during plugin initialization will
be re-raised as PluginAttributeErrors, and the original traceback will
be preserved. This gives much more useful information to plugin
developers, as in
Traceback (most recent call last):
File ".../openstackclient/shell.py", line 118, in run
ret_val = super(OpenStackShell, self).run(argv)
...
File ".../openstackclient/object/v1/container.py", line 150, in take_action
data = self.app.client_manager.object_store.container_list(
File ".../openstackclient/common/clientmanager.py", line 57, in __get__
err_val, err_tb)
File ".../openstackclient/common/clientmanager.py", line 51, in __get__
self._handle = self.factory(instance)
File ".../openstackclient/object/client.py", line 35, in make_client
interface=instance._interface,
File ".../openstackclient/common/clientmanager.py", line 258,
in get_endpoint_for_service_type
endpoint = self.auth_ref.service_catalog.url_for(
PluginAttributeError: 'NoneType' object has no attribute 'url_for'
Change-Id: I0eee7eba6eccc6d471a699a381185c4e76da10bd
2016-04-14 21:18:16 +00:00
|
|
|
import six
|
2014-07-18 17:18:25 +00:00
|
|
|
|
2016-06-22 17:46:38 +00:00
|
|
|
from osc_lib.api import auth
|
2016-05-10 19:57:04 +00:00
|
|
|
from osc_lib import exceptions
|
|
|
|
from osc_lib import session as osc_session
|
2016-10-18 04:52:09 +00:00
|
|
|
from osc_lib import version
|
2012-05-02 21:02:08 +00:00
|
|
|
|
2013-01-31 19:31:41 +00:00
|
|
|
|
2012-05-02 21:02:08 +00:00
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
2014-10-13 16:13:48 +00:00
|
|
|
PLUGIN_MODULES = []
|
|
|
|
|
2012-05-02 21:02:08 +00:00
|
|
|
|
|
|
|
class ClientCache(object):
|
2013-01-31 19:31:41 +00:00
|
|
|
"""Descriptor class for caching created client handles."""
|
2016-02-23 16:38:58 +00:00
|
|
|
|
2012-05-02 21:02:08 +00:00
|
|
|
def __init__(self, factory):
|
|
|
|
self.factory = factory
|
|
|
|
self._handle = None
|
|
|
|
|
|
|
|
def __get__(self, instance, owner):
|
|
|
|
# Tell the ClientManager to login to keystone
|
|
|
|
if self._handle is None:
|
Propagate AttributeErrors when lazily loading plugins
Previously, if an AttributeError was raised in a plugin's make_client
method, the plugin simply wouldn't be an attribute of the ClientManager,
producing tracebacks like
Traceback (most recent call last):
File ".../openstackclient/shell.py", line 118, in run
ret_val = super(OpenStackShell, self).run(argv)
...
File ".../openstackclient/object/v1/container.py", line 150, in take_action
data = self.app.client_manager.object_store.container_list(
File ".../openstackclient/common/clientmanager.py", line 66, in __getattr__
raise AttributeError(name)
AttributeError: object_store
This made writing minimal third-party auth plugins difficult, as it
obliterated the original AttributeError.
Now, AttributeErrors that are raised during plugin initialization will
be re-raised as PluginAttributeErrors, and the original traceback will
be preserved. This gives much more useful information to plugin
developers, as in
Traceback (most recent call last):
File ".../openstackclient/shell.py", line 118, in run
ret_val = super(OpenStackShell, self).run(argv)
...
File ".../openstackclient/object/v1/container.py", line 150, in take_action
data = self.app.client_manager.object_store.container_list(
File ".../openstackclient/common/clientmanager.py", line 57, in __get__
err_val, err_tb)
File ".../openstackclient/common/clientmanager.py", line 51, in __get__
self._handle = self.factory(instance)
File ".../openstackclient/object/client.py", line 35, in make_client
interface=instance._interface,
File ".../openstackclient/common/clientmanager.py", line 258,
in get_endpoint_for_service_type
endpoint = self.auth_ref.service_catalog.url_for(
PluginAttributeError: 'NoneType' object has no attribute 'url_for'
Change-Id: I0eee7eba6eccc6d471a699a381185c4e76da10bd
2016-04-14 21:18:16 +00:00
|
|
|
try:
|
|
|
|
self._handle = self.factory(instance)
|
|
|
|
except AttributeError as err:
|
|
|
|
# Make sure the failure propagates. Otherwise, the plugin just
|
|
|
|
# quietly isn't there.
|
|
|
|
new_err = exceptions.PluginAttributeError(err)
|
|
|
|
six.reraise(new_err.__class__, new_err, sys.exc_info()[2])
|
2012-05-02 21:02:08 +00:00
|
|
|
return self._handle
|
|
|
|
|
|
|
|
|
|
|
|
class ClientManager(object):
|
2013-01-31 19:31:41 +00:00
|
|
|
"""Manages access to API clients, including authentication."""
|
2015-07-22 15:51:07 +00:00
|
|
|
|
2014-10-20 23:53:10 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2015-03-02 23:05:35 +00:00
|
|
|
cli_options=None,
|
2014-10-20 23:53:10 +00:00
|
|
|
api_version=None,
|
|
|
|
pw_func=None,
|
2016-10-18 04:52:09 +00:00
|
|
|
app_name=None,
|
|
|
|
app_version=None,
|
2014-10-20 23:53:10 +00:00
|
|
|
):
|
|
|
|
"""Set up a ClientManager
|
|
|
|
|
2015-02-27 15:19:12 +00:00
|
|
|
:param cli_options:
|
2014-10-20 23:53:10 +00:00
|
|
|
Options collected from the command-line, environment, or wherever
|
|
|
|
:param api_version:
|
|
|
|
Dict of API versions: key is API name, value is the version
|
|
|
|
:param pw_func:
|
|
|
|
Callback function for asking the user for a password. The function
|
|
|
|
takes an optional string for the prompt ('Password: ' on None) and
|
2015-01-16 07:20:52 +00:00
|
|
|
returns a string containing the password
|
2016-10-18 04:52:09 +00:00
|
|
|
:param app_name:
|
|
|
|
The name of the application for passing through to the useragent
|
|
|
|
:param app_version:
|
|
|
|
The version of the application for passing through to the useragent
|
2014-10-20 23:53:10 +00:00
|
|
|
"""
|
|
|
|
|
2015-02-27 15:19:12 +00:00
|
|
|
self._cli_options = cli_options
|
|
|
|
self._api_version = api_version
|
|
|
|
self._pw_callback = pw_func
|
2016-10-18 04:52:09 +00:00
|
|
|
self._app_name = app_name
|
|
|
|
self._app_version = app_version
|
2016-05-10 22:32:10 +00:00
|
|
|
self.region_name = self._cli_options.region_name
|
|
|
|
self.interface = self._cli_options.interface
|
2015-02-27 15:19:12 +00:00
|
|
|
|
|
|
|
self.timing = self._cli_options.timing
|
|
|
|
|
|
|
|
self._auth_ref = None
|
|
|
|
self.session = None
|
|
|
|
|
2016-05-10 22:32:10 +00:00
|
|
|
# self.verify is the Requests-compatible form
|
|
|
|
# self.cacert is the form used by the legacy client libs
|
|
|
|
# self.insecure is not needed, use 'not self.verify'
|
|
|
|
|
|
|
|
# NOTE(dtroyer): Per bug https://bugs.launchpad.net/bugs/1447784
|
|
|
|
# --insecure overrides any --os-cacert setting
|
|
|
|
|
2016-08-25 14:35:34 +00:00
|
|
|
# Set a hard default
|
|
|
|
self.verify = True
|
2016-05-10 22:32:10 +00:00
|
|
|
if self._cli_options.insecure:
|
|
|
|
# Handle --insecure
|
|
|
|
self.verify = False
|
|
|
|
self.cacert = None
|
2015-02-27 15:19:12 +00:00
|
|
|
else:
|
2016-05-10 22:32:10 +00:00
|
|
|
if (self._cli_options.cacert is not None
|
|
|
|
and self._cli_options.cacert != ''):
|
|
|
|
# --cacert implies --verify here
|
|
|
|
self.verify = self._cli_options.cacert
|
|
|
|
self.cacert = self._cli_options.cacert
|
|
|
|
else:
|
|
|
|
# Fall through also gets --verify
|
2016-08-25 14:35:34 +00:00
|
|
|
if self._cli_options.verify is not None:
|
|
|
|
self.verify = self._cli_options.verify
|
2016-05-10 22:32:10 +00:00
|
|
|
self.cacert = None
|
2015-02-27 15:19:12 +00:00
|
|
|
|
2016-04-01 21:42:27 +00:00
|
|
|
# Set up client certificate and key
|
|
|
|
# NOTE(cbrandily): This converts client certificate/key to requests
|
|
|
|
# cert argument: None (no client certificate), a path
|
|
|
|
# to client certificate or a tuple with client
|
|
|
|
# certificate/key paths.
|
2016-05-10 22:32:10 +00:00
|
|
|
self.cert = self._cli_options.cert
|
|
|
|
if self.cert and self._cli_options.key:
|
|
|
|
self.cert = self.cert, self._cli_options.key
|
2016-04-01 21:42:27 +00:00
|
|
|
|
2015-02-27 15:19:12 +00:00
|
|
|
# Get logging from root logger
|
|
|
|
root_logger = logging.getLogger('')
|
|
|
|
LOG.setLevel(root_logger.getEffectiveLevel())
|
|
|
|
|
2016-02-08 19:16:24 +00:00
|
|
|
# NOTE(gyee): use this flag to indicate whether auth setup has already
|
|
|
|
# been completed. If so, do not perform auth setup again. The reason
|
|
|
|
# we need this flag is that we want to be able to perform auth setup
|
|
|
|
# outside of auth_ref as auth_ref itself is a property. We can not
|
|
|
|
# retrofit auth_ref to optionally skip scope check. Some operations
|
|
|
|
# do not require a scoped token. In those cases, we call setup_auth
|
|
|
|
# prior to dereferrencing auth_ref.
|
|
|
|
self._auth_setup_completed = False
|
|
|
|
|
2016-06-22 18:37:25 +00:00
|
|
|
def setup_auth(self):
|
2015-02-27 15:19:12 +00:00
|
|
|
"""Set up authentication
|
|
|
|
|
|
|
|
This is deferred until authentication is actually attempted because
|
|
|
|
it gets in the way of things that do not require auth.
|
|
|
|
"""
|
|
|
|
|
2016-02-08 19:16:24 +00:00
|
|
|
if self._auth_setup_completed:
|
|
|
|
return
|
|
|
|
|
2016-06-24 14:17:32 +00:00
|
|
|
# Stash the selected auth type
|
|
|
|
self.auth_plugin_name = self._cli_options.config['auth_type']
|
2014-10-20 23:53:10 +00:00
|
|
|
|
2015-01-16 07:20:52 +00:00
|
|
|
# Basic option checking to avoid unhelpful error messages
|
2016-06-22 18:37:25 +00:00
|
|
|
auth.check_valid_authentication_options(
|
|
|
|
self._cli_options,
|
|
|
|
self.auth_plugin_name,
|
|
|
|
)
|
2015-01-16 07:20:52 +00:00
|
|
|
|
2014-10-20 23:53:10 +00:00
|
|
|
# Horrible hack alert...must handle prompt for null password if
|
|
|
|
# password auth is requested.
|
2014-10-22 16:12:47 +00:00
|
|
|
if (self.auth_plugin_name.endswith('password') and
|
2016-02-20 06:28:08 +00:00
|
|
|
not self._cli_options.auth.get('password')):
|
2015-07-13 13:44:24 +00:00
|
|
|
self._cli_options.auth['password'] = self._pw_callback()
|
2014-10-20 23:53:10 +00:00
|
|
|
|
2016-02-20 06:09:40 +00:00
|
|
|
LOG.info('Using auth plugin: %s', self.auth_plugin_name)
|
|
|
|
LOG.debug('Using parameters %s',
|
2016-06-24 14:17:32 +00:00
|
|
|
strutils.mask_password(self._cli_options.auth))
|
|
|
|
self.auth = self._cli_options.get_auth()
|
2016-09-16 16:47:04 +00:00
|
|
|
|
|
|
|
if self._cli_options.service_provider:
|
|
|
|
self.auth = auth.get_keystone2keystone_auth(
|
|
|
|
self.auth,
|
|
|
|
self._cli_options.service_provider,
|
|
|
|
self._cli_options.remote_project_id,
|
|
|
|
self._cli_options.remote_project_name,
|
|
|
|
self._cli_options.remote_project_domain_id,
|
|
|
|
self._cli_options.remote_project_domain_name
|
|
|
|
)
|
|
|
|
|
2014-09-05 07:00:36 +00:00
|
|
|
self.session = osc_session.TimingSession(
|
2014-10-09 20:16:07 +00:00
|
|
|
auth=self.auth,
|
2016-05-10 22:32:10 +00:00
|
|
|
verify=self.verify,
|
|
|
|
cert=self.cert,
|
2016-10-18 04:52:09 +00:00
|
|
|
app_name=self._app_name,
|
|
|
|
app_version=self._app_version,
|
|
|
|
additional_user_agent=[('osc-lib', version.version_string)],
|
2014-10-09 20:16:07 +00:00
|
|
|
)
|
2016-02-08 19:16:24 +00:00
|
|
|
self._auth_setup_completed = True
|
|
|
|
|
2016-06-22 18:37:25 +00:00
|
|
|
def validate_scope(self):
|
|
|
|
if self._auth_ref.project_id is not None:
|
|
|
|
# We already have a project scope.
|
|
|
|
return
|
|
|
|
if self._auth_ref.domain_id is not None:
|
|
|
|
# We already have a domain scope.
|
|
|
|
return
|
|
|
|
|
|
|
|
# We do not have a scoped token (and the user's default project scope
|
|
|
|
# was not implied), so the client needs to be explicitly configured
|
|
|
|
# with a scope.
|
|
|
|
auth.check_valid_authorization_options(
|
|
|
|
self._cli_options,
|
|
|
|
self.auth_plugin_name,
|
|
|
|
)
|
|
|
|
|
2014-10-20 23:53:10 +00:00
|
|
|
@property
|
|
|
|
def auth_ref(self):
|
|
|
|
"""Dereference will trigger an auth if it hasn't already"""
|
|
|
|
if not self._auth_ref:
|
2015-02-27 15:19:12 +00:00
|
|
|
self.setup_auth()
|
2014-10-20 23:53:10 +00:00
|
|
|
LOG.debug("Get auth_ref")
|
|
|
|
self._auth_ref = self.auth.get_auth_ref(self.session)
|
|
|
|
return self._auth_ref
|
|
|
|
|
2016-05-21 00:38:41 +00:00
|
|
|
def is_service_available(self, service_type):
|
|
|
|
"""Check if a service type is in the current Service Catalog"""
|
|
|
|
|
|
|
|
# Trigger authentication necessary to discover endpoint
|
2015-12-02 20:43:01 +00:00
|
|
|
if self.auth_ref:
|
|
|
|
service_catalog = self.auth_ref.service_catalog
|
|
|
|
else:
|
|
|
|
service_catalog = None
|
|
|
|
# Assume that the network endpoint is enabled.
|
2016-05-21 00:38:41 +00:00
|
|
|
service_available = None
|
2015-12-02 20:43:01 +00:00
|
|
|
if service_catalog:
|
2016-05-21 00:38:41 +00:00
|
|
|
if service_type in service_catalog.get_endpoints():
|
|
|
|
service_available = True
|
|
|
|
LOG.debug("%s endpoint in service catalog", service_type)
|
2015-12-02 20:43:01 +00:00
|
|
|
else:
|
2016-05-21 00:38:41 +00:00
|
|
|
service_available = False
|
|
|
|
LOG.debug("No %s endpoint in service catalog", service_type)
|
2015-12-02 20:43:01 +00:00
|
|
|
else:
|
2016-05-21 00:38:41 +00:00
|
|
|
LOG.debug("No service catalog")
|
|
|
|
return service_available
|
2015-12-02 20:43:01 +00:00
|
|
|
|
2015-05-22 23:22:35 +00:00
|
|
|
def get_endpoint_for_service_type(self, service_type, region_name=None,
|
2015-07-04 15:32:16 +00:00
|
|
|
interface='public'):
|
2013-01-31 19:31:41 +00:00
|
|
|
"""Return the endpoint URL for the service type."""
|
2015-07-04 15:32:16 +00:00
|
|
|
if not interface:
|
|
|
|
interface = 'public'
|
2012-05-02 21:02:08 +00:00
|
|
|
# See if we are using password flow auth, i.e. we have a
|
|
|
|
# service catalog to select endpoints from
|
2014-10-18 04:43:38 +00:00
|
|
|
if self.auth_ref:
|
|
|
|
endpoint = self.auth_ref.service_catalog.url_for(
|
|
|
|
service_type=service_type,
|
|
|
|
region_name=region_name,
|
2016-06-13 16:13:43 +00:00
|
|
|
interface=interface,
|
2014-10-18 04:43:38 +00:00
|
|
|
)
|
2012-05-02 21:02:08 +00:00
|
|
|
else:
|
2014-10-18 04:43:38 +00:00
|
|
|
# Get the passed endpoint directly from the auth plugin
|
2016-06-13 16:13:43 +00:00
|
|
|
endpoint = self.auth.get_endpoint(
|
|
|
|
self.session,
|
|
|
|
interface=interface,
|
|
|
|
)
|
2012-05-02 21:02:08 +00:00
|
|
|
return endpoint
|
2013-11-21 00:02:09 +00:00
|
|
|
|
2015-07-19 18:15:04 +00:00
|
|
|
def get_configuration(self):
|
|
|
|
return copy.deepcopy(self._cli_options.config)
|