Skip to content

Commit f38c51c

Browse files
Dean Troyerstevemar
authored andcommitted
Rework clientmanager
* Add compatibility for plugin v2 interface removed from osc-lib * ClientManager.is_network_endpoint_enabled() is wrapper for new is_service_available() Change-Id: I6f26ce9e4d0702f50c7949bacfbeeb0f98cddb5d
1 parent 719c5d7 commit f38c51c

3 files changed

Lines changed: 55 additions & 600 deletions

File tree

openstackclient/common/clientmanager.py

Lines changed: 23 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,13 @@
1515

1616
"""Manage access to the clients, including authenticating when needed."""
1717

18-
import copy
1918
import logging
2019
import pkg_resources
2120
import sys
2221

2322
from keystoneauth1.loading import base
2423
from osc_lib.api import auth
25-
from osc_lib import exceptions
26-
from oslo_utils import strutils
27-
import requests
28-
import six
29-
30-
from openstackclient.common import session as osc_session
31-
from openstackclient.identity import client as identity_client
24+
from osc_lib import clientmanager
3225

3326

3427
LOG = logging.getLogger(__name__)
@@ -109,267 +102,44 @@ def build_auth_params(auth_plugin_name, cmd_options):
109102
return (auth_plugin_loader, auth_params)
110103

111104

112-
class ClientCache(object):
113-
"""Descriptor class for caching created client handles."""
114-
115-
def __init__(self, factory):
116-
self.factory = factory
117-
self._handle = None
118-
119-
def __get__(self, instance, owner):
120-
# Tell the ClientManager to login to keystone
121-
if self._handle is None:
122-
try:
123-
self._handle = self.factory(instance)
124-
except AttributeError as err:
125-
# Make sure the failure propagates. Otherwise, the plugin just
126-
# quietly isn't there.
127-
new_err = exceptions.PluginAttributeError(err)
128-
six.reraise(new_err.__class__, new_err, sys.exc_info()[2])
129-
return self._handle
105+
class ClientManager(clientmanager.ClientManager):
106+
"""Manages access to API clients, including authentication
130107
131-
132-
class ClientManager(object):
133-
"""Manages access to API clients, including authentication."""
108+
Wrap osc_lib's ClientManager to maintain compatibility for the existing
109+
plugin V2 interface. Some currently private attributes become public
110+
in osc-lib so we need to maintain a transition period.
111+
"""
134112

135113
# A simple incrementing version for the plugin to know what is available
136114
PLUGIN_INTERFACE_VERSION = "2"
137115

138-
identity = ClientCache(identity_client.make_client)
139-
140-
def __getattr__(self, name):
141-
# this is for the auth-related parameters.
142-
if name in ['_' + o.replace('-', '_')
143-
for o in auth.OPTIONS_LIST]:
144-
return self._auth_params[name[1:]]
145-
146-
raise AttributeError(name)
147-
148116
def __init__(
149117
self,
150118
cli_options=None,
151119
api_version=None,
152-
verify=True,
153120
pw_func=None,
154121
):
155-
"""Set up a ClientManager
156-
157-
:param cli_options:
158-
Options collected from the command-line, environment, or wherever
159-
:param api_version:
160-
Dict of API versions: key is API name, value is the version
161-
:param verify:
162-
TLS certificate verification; may be a boolean to enable or disable
163-
server certificate verification, or a filename of a CA certificate
164-
bundle to be used in verification (implies True)
165-
:param pw_func:
166-
Callback function for asking the user for a password. The function
167-
takes an optional string for the prompt ('Password: ' on None) and
168-
returns a string containing the password
169-
"""
170-
171-
self._cli_options = cli_options
172-
self._api_version = api_version
173-
self._pw_callback = pw_func
174-
self._url = self._cli_options.auth.get('url')
175-
self._region_name = self._cli_options.region_name
176-
self._interface = self._cli_options.interface
177-
178-
self.timing = self._cli_options.timing
179-
180-
self._auth_ref = None
181-
self.session = None
182-
183-
# verify is the Requests-compatible form
184-
self._verify = verify
185-
# also store in the form used by the legacy client libs
186-
self._cacert = None
187-
if isinstance(verify, bool):
188-
self._insecure = not verify
189-
else:
190-
self._cacert = verify
191-
self._insecure = False
192-
193-
# Set up client certificate and key
194-
# NOTE(cbrandily): This converts client certificate/key to requests
195-
# cert argument: None (no client certificate), a path
196-
# to client certificate or a tuple with client
197-
# certificate/key paths.
198-
self._cert = self._cli_options.cert
199-
if self._cert and self._cli_options.key:
200-
self._cert = self._cert, self._cli_options.key
201-
202-
# Get logging from root logger
203-
root_logger = logging.getLogger('')
204-
LOG.setLevel(root_logger.getEffectiveLevel())
205-
206-
# NOTE(gyee): use this flag to indicate whether auth setup has already
207-
# been completed. If so, do not perform auth setup again. The reason
208-
# we need this flag is that we want to be able to perform auth setup
209-
# outside of auth_ref as auth_ref itself is a property. We can not
210-
# retrofit auth_ref to optionally skip scope check. Some operations
211-
# do not require a scoped token. In those cases, we call setup_auth
212-
# prior to dereferrencing auth_ref.
213-
self._auth_setup_completed = False
214-
215-
def _set_default_scope_options(self):
216-
# TODO(mordred): This is a usability improvement that's broadly useful
217-
# We should port it back up into os-client-config.
218-
default_domain = self._cli_options.default_domain
219-
220-
# NOTE(hieulq): If USER_DOMAIN_NAME, USER_DOMAIN_ID, PROJECT_DOMAIN_ID
221-
# or PROJECT_DOMAIN_NAME is present and API_VERSION is 2.0, then
222-
# ignore all domain related configs.
223-
if (self._api_version.get('identity') == '2.0' and
224-
self.auth_plugin_name.endswith('password')):
225-
domain_props = ['project_domain_name', 'project_domain_id',
226-
'user_domain_name', 'user_domain_id']
227-
for prop in domain_props:
228-
if self._auth_params.pop(prop, None) is not None:
229-
LOG.warning("Ignoring domain related configs " +
230-
prop + " because identity API version is 2.0")
231-
return
232-
233-
# NOTE(aloga): The scope parameters below only apply to v3 and v3
234-
# related auth plugins, so we stop the parameter checking if v2 is
235-
# being used.
236-
if (self._api_version.get('identity') != '3' or
237-
self.auth_plugin_name.startswith('v2')):
238-
return
239-
240-
# NOTE(stevemar): If PROJECT_DOMAIN_ID or PROJECT_DOMAIN_NAME is
241-
# present, then do not change the behaviour. Otherwise, set the
242-
# PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability.
243-
if ('project_domain_id' in self._auth_params and
244-
not self._auth_params.get('project_domain_id') and
245-
not self._auth_params.get('project_domain_name')):
246-
self._auth_params['project_domain_id'] = default_domain
247-
248-
# NOTE(stevemar): If USER_DOMAIN_ID or USER_DOMAIN_NAME is present,
249-
# then do not change the behaviour. Otherwise, set the
250-
# USER_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability.
251-
if ('user_domain_id' in self._auth_params and
252-
not self._auth_params.get('user_domain_id') and
253-
not self._auth_params.get('user_domain_name')):
254-
self._auth_params['user_domain_id'] = default_domain
255-
256-
def setup_auth(self):
257-
"""Set up authentication
258-
259-
This is deferred until authentication is actually attempted because
260-
it gets in the way of things that do not require auth.
261-
"""
262-
263-
if self._auth_setup_completed:
264-
return
265-
266-
# If no auth type is named by the user, select one based on
267-
# the supplied options
268-
self.auth_plugin_name = select_auth_plugin(self._cli_options)
269-
270-
# Basic option checking to avoid unhelpful error messages
271-
auth.check_valid_authentication_options(self._cli_options,
272-
self.auth_plugin_name)
273-
274-
# Horrible hack alert...must handle prompt for null password if
275-
# password auth is requested.
276-
if (self.auth_plugin_name.endswith('password') and
277-
not self._cli_options.auth.get('password')):
278-
self._cli_options.auth['password'] = self._pw_callback()
279-
280-
(auth_plugin, self._auth_params) = build_auth_params(
281-
self.auth_plugin_name,
282-
self._cli_options,
122+
super(ClientManager, self).__init__(
123+
cli_options=cli_options,
124+
api_version=api_version,
125+
pw_func=pw_func,
283126
)
284127

285-
self._set_default_scope_options()
286-
287-
# For compatibility until all clients can be updated
288-
if 'project_name' in self._auth_params:
289-
self._project_name = self._auth_params['project_name']
290-
elif 'tenant_name' in self._auth_params:
291-
self._project_name = self._auth_params['tenant_name']
292-
293-
LOG.info('Using auth plugin: %s', self.auth_plugin_name)
294-
LOG.debug('Using parameters %s',
295-
strutils.mask_password(self._auth_params))
296-
self.auth = auth_plugin.load_from_options(**self._auth_params)
297-
# needed by SAML authentication
298-
request_session = requests.session()
299-
self.session = osc_session.TimingSession(
300-
auth=self.auth,
301-
session=request_session,
302-
verify=self._verify,
303-
cert=self._cert,
304-
user_agent=USER_AGENT,
305-
)
306-
307-
self._auth_setup_completed = True
308-
309-
def validate_scope(self):
310-
if self._auth_ref.project_id is not None:
311-
# We already have a project scope.
312-
return
313-
if self._auth_ref.domain_id is not None:
314-
# We already have a domain scope.
315-
return
316-
317-
# We do not have a scoped token (and the user's default project scope
318-
# was not implied), so the client needs to be explicitly configured
319-
# with a scope.
320-
auth.check_valid_authorization_options(self._cli_options,
321-
self.auth_plugin_name)
322-
323-
@property
324-
def auth_ref(self):
325-
"""Dereference will trigger an auth if it hasn't already"""
326-
if not self._auth_ref:
327-
self.setup_auth()
328-
LOG.debug("Get auth_ref")
329-
self._auth_ref = self.auth.get_auth_ref(self.session)
330-
return self._auth_ref
128+
# TODO(dtroyer): For compatibility; mark this for removal when plugin
129+
# interface v2 is removed
130+
self._region_name = self.region_name
131+
self._interface = self.interface
132+
self._cacert = self.cacert
133+
self._insecure = not self.verify
331134

332135
def is_network_endpoint_enabled(self):
333136
"""Check if the network endpoint is enabled"""
334-
# Trigger authentication necessary to determine if the network
335-
# endpoint is enabled.
336-
if self.auth_ref:
337-
service_catalog = self.auth_ref.service_catalog
338-
else:
339-
service_catalog = None
340-
# Assume that the network endpoint is enabled.
341-
network_endpoint_enabled = True
342-
if service_catalog:
343-
if 'network' in service_catalog.get_endpoints():
344-
LOG.debug("Network endpoint in service catalog")
345-
else:
346-
LOG.debug("No network endpoint in service catalog")
347-
network_endpoint_enabled = False
348-
else:
349-
LOG.debug("No service catalog, assuming network endpoint enabled")
350-
return network_endpoint_enabled
351-
352-
def get_endpoint_for_service_type(self, service_type, region_name=None,
353-
interface='public'):
354-
"""Return the endpoint URL for the service type."""
355-
if not interface:
356-
interface = 'public'
357-
# See if we are using password flow auth, i.e. we have a
358-
# service catalog to select endpoints from
359-
if self.auth_ref:
360-
endpoint = self.auth_ref.service_catalog.url_for(
361-
service_type=service_type,
362-
region_name=region_name,
363-
interface=interface,
364-
)
365-
else:
366-
# Get the passed endpoint directly from the auth plugin
367-
endpoint = self.auth.get_endpoint(self.session,
368-
interface=interface)
369-
return endpoint
370137

371-
def get_configuration(self):
372-
return copy.deepcopy(self._cli_options.config)
138+
# NOTE(dtroyer): is_service_available() can also return None if
139+
# there is no Service Catalog, callers here are
140+
# not expecting that so fold None into True to
141+
# use Network API by default
142+
return self.is_service_available('network') is not False
373143

374144

375145
# Plugin Support
@@ -391,7 +161,7 @@ def get_plugin_modules(group):
391161
setattr(
392162
ClientManager,
393163
module.API_NAME,
394-
ClientCache(
164+
clientmanager.ClientCache(
395165
getattr(sys.modules[ep.module_name], 'make_client', None)
396166
),
397167
)

openstackclient/shell.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from cliff import command
2727
from cliff import complete
2828
from cliff import help
29+
from osc_lib.cli import client_config as cloud_config
2930
from osc_lib.command import timing
3031
from osc_lib import exceptions as exc
3132
from osc_lib import logs
@@ -38,8 +39,6 @@
3839
from openstackclient.common import commandmanager
3940
from openstackclient.i18n import _
4041

41-
from os_client_config import config as cloud_config
42-
4342
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
4443

4544

@@ -309,6 +308,9 @@ def initialize_app(self, argv):
309308
tenant_id = getattr(self.options, 'tenant_id', None)
310309
tenant_name = getattr(self.options, 'tenant_name', None)
311310

311+
# Save default domain
312+
self.default_domain = self.options.default_domain
313+
312314
# handle some v2/v3 authentication inconsistencies by just acting like
313315
# both the project and tenant information are both present. This can
314316
# go away if we stop registering all the argparse options together.
@@ -325,7 +327,7 @@ def initialize_app(self, argv):
325327
# Ignore the default value of interface. Only if it is set later
326328
# will it be used.
327329
try:
328-
cc = cloud_config.OpenStackConfig(
330+
cc = cloud_config.OSC_Config(
329331
override_defaults={
330332
'interface': None,
331333
'auth_type': auth_type,
@@ -368,9 +370,6 @@ def initialize_app(self, argv):
368370
if self.verify and self.cloud.cacert:
369371
self.verify = self.cloud.cacert
370372

371-
# Save default domain
372-
self.default_domain = self.options.default_domain
373-
374373
# Loop through extensions to get API versions
375374
for mod in clientmanager.PLUGIN_MODULES:
376375
default_version = getattr(mod, 'DEFAULT_API_VERSION', None)
@@ -429,7 +428,6 @@ def initialize_app(self, argv):
429428

430429
self.client_manager = clientmanager.ClientManager(
431430
cli_options=self.cloud,
432-
verify=self.verify,
433431
api_version=self.api_version,
434432
pw_func=prompt_for_password,
435433
)

0 commit comments

Comments
 (0)