1515
1616"""Manage access to the clients, including authenticating when needed."""
1717
18- import copy
1918import logging
2019import pkg_resources
2120import sys
2221
2322from keystoneauth1 .loading import base
2423from 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
3427LOG = 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 )
0 commit comments