Skip to content

Commit d1e1f8d

Browse files
committed
Stop lazy importing keystoneclient
There were two basic problems: - We'd try to import on every attempt at getting auth, even when we already know keystoneclient isn't available. - Sometimes devs would hit some crazy import race involving (some combination of?) greenthreads and OS threads. So let's just try the imports *once*, at import time, and have None sentinels if it fails. Try both versions separately to decouple failures; this should let us support a wider range of keystoneclient versions. Change-Id: I2367310aac74f1b7c5ea0cb1a822a491e4ba8e68
1 parent 4330d03 commit d1e1f8d

File tree

4 files changed

+68
-82
lines changed

4 files changed

+68
-82
lines changed

swiftclient/client.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ def emit(self, record):
6060
def createLock(self):
6161
self.lock = None
6262

63+
ksexceptions = ksclient_v2 = ksclient_v3 = None
64+
try:
65+
from keystoneclient import exceptions as ksexceptions
66+
# prevent keystoneclient warning us that it has no log handlers
67+
logging.getLogger('keystoneclient').addHandler(NullHandler())
68+
from keystoneclient.v2_0 import client as ksclient_v2
69+
except ImportError:
70+
pass
71+
try:
72+
from keystoneclient.v3 import client as ksclient_v3
73+
except ImportError:
74+
pass
75+
6376
# requests version 1.2.3 try to encode headers in ascii, preventing
6477
# utf-8 encoded header to be 'prepared'
6578
if StrictVersion(requests.__version__) < StrictVersion('2.0.0'):
@@ -540,25 +553,6 @@ def get_keystoneclient_2_0(auth_url, user, key, os_options, **kwargs):
540553
return get_auth_keystone(auth_url, user, key, os_options, **kwargs)
541554

542555

543-
def _import_keystone_client(auth_version):
544-
# the attempted imports are encapsulated in this function to allow
545-
# mocking for tests
546-
try:
547-
if auth_version in AUTH_VERSIONS_V3:
548-
from keystoneclient.v3 import client as ksclient
549-
else:
550-
from keystoneclient.v2_0 import client as ksclient
551-
from keystoneclient import exceptions
552-
# prevent keystoneclient warning us that it has no log handlers
553-
logging.getLogger('keystoneclient').addHandler(NullHandler())
554-
return ksclient, exceptions
555-
except ImportError:
556-
raise ClientException('''
557-
Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth
558-
version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment
559-
variables to be set or overridden with -A, -U, or -K.''')
560-
561-
562556
def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
563557
"""
564558
Authenticate against a keystone server.
@@ -587,7 +581,20 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
587581
# Legacy default if not set
588582
if auth_version is None:
589583
auth_version = '2'
590-
ksclient, exceptions = _import_keystone_client(auth_version)
584+
585+
ksclient = None
586+
if auth_version in AUTH_VERSIONS_V3:
587+
if ksclient_v3 is not None:
588+
ksclient = ksclient_v3
589+
else:
590+
if ksclient_v2 is not None:
591+
ksclient = ksclient_v2
592+
593+
if ksclient is None:
594+
raise ClientException('''
595+
Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth
596+
version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment
597+
variables to be set or overridden with -A, -U, or -K.''')
591598

592599
try:
593600
_ksclient = ksclient.Client(
@@ -608,13 +615,13 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
608615
cert=kwargs.get('cert'),
609616
key=kwargs.get('cert_key'),
610617
auth_url=auth_url, insecure=insecure, timeout=timeout)
611-
except exceptions.Unauthorized:
618+
except ksexceptions.Unauthorized:
612619
msg = 'Unauthorized. Check username, password and tenant name/id.'
613620
if auth_version in AUTH_VERSIONS_V3:
614621
msg = ('Unauthorized. Check username/id, password, '
615622
'tenant name/id and user/tenant domain name/id.')
616623
raise ClientException(msg)
617-
except exceptions.AuthorizationFailure as err:
624+
except ksexceptions.AuthorizationFailure as err:
618625
raise ClientException('Authorization Failure. %s' % err)
619626
service_type = os_options.get('service_type') or 'object-store'
620627
endpoint_type = os_options.get('endpoint_type') or 'publicURL'
@@ -627,7 +634,7 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
627634
service_type=service_type,
628635
endpoint_type=endpoint_type,
629636
**filter_kwargs)
630-
except exceptions.EndpointNotFound:
637+
except ksexceptions.EndpointNotFound:
631638
raise ClientException('Endpoint for %s not found - '
632639
'have you specified a region?' % service_type)
633640
return endpoint, _ksclient.auth_token

tests/unit/test_shell.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
from os.path import basename, dirname
3939
from .utils import (
40-
CaptureOutput, fake_get_auth_keystone, _make_fake_import_keystone_client,
40+
CaptureOutput, fake_get_auth_keystone,
4141
FakeKeystone, StubResponse, MockHttpTest)
4242
from swiftclient.utils import (
4343
EMPTY_ETAG, EXPIRES_ISO8601_FORMAT,
@@ -2534,16 +2534,26 @@ def _test_options_passed_to_keystone(self, cmd, opts, os_opts,
25342534
cmd_args=cmd_args)
25352535
ks_endpoint = 'http://example.com:8080/v1/AUTH_acc'
25362536
ks_token = 'fake_auth_token'
2537+
# check correct auth version gets used
2538+
key = 'auth-version'
25372539
fake_ks = FakeKeystone(endpoint=ks_endpoint, token=ks_token)
2540+
if no_auth:
2541+
fake_ks2 = fake_ks3 = None
2542+
elif opts.get(key, self.defaults.get(key)) == '2.0':
2543+
fake_ks2 = fake_ks
2544+
fake_ks3 = None
2545+
else:
2546+
fake_ks2 = None
2547+
fake_ks3 = fake_ks
25382548
# fake_conn will check that storage_url and auth_token are as expected
25392549
endpoint = os_opts.get('storage-url', ks_endpoint)
25402550
token = os_opts.get('auth-token', ks_token)
25412551
fake_conn = self.fake_http_connection(204, headers={},
25422552
storage_url=endpoint,
25432553
auth_token=token)
25442554

2545-
with mock.patch('swiftclient.client._import_keystone_client',
2546-
_make_fake_import_keystone_client(fake_ks)), \
2555+
with mock.patch('swiftclient.client.ksclient_v2', fake_ks2), \
2556+
mock.patch('swiftclient.client.ksclient_v3', fake_ks3), \
25472557
mock.patch('swiftclient.client.http_connection', fake_conn), \
25482558
mock.patch.dict(os.environ, env, clear=True), \
25492559
patch_disable_warnings() as mock_disable_warnings:
@@ -2562,16 +2572,11 @@ def _test_options_passed_to_keystone(self, cmd, opts, os_opts,
25622572
self.assertEqual([], mock_disable_warnings.mock_calls)
25632573

25642574
if no_auth:
2565-
# check that keystone client was not used and terminate tests
2566-
self.assertIsNone(getattr(fake_ks, 'auth_version'))
2567-
self.assertEqual(len(fake_ks.calls), 0)
2575+
# We patched out both keystoneclient versions to be None;
2576+
# they *can't* have been used and if we tried to, we would
2577+
# have raised ClientExceptions
25682578
return
25692579

2570-
# check correct auth version was passed to _import_keystone_client
2571-
key = 'auth-version'
2572-
expected = opts.get(key, self.defaults.get(key))
2573-
self.assertEqual(expected, fake_ks.auth_version)
2574-
25752580
# check args passed to keystone Client __init__
25762581
self.assertEqual(len(fake_ks.calls), 1)
25772582
actual_args = fake_ks.calls[0]
@@ -2942,9 +2947,9 @@ def setUp(self):
29422947
self.account = 'AUTH_alice'
29432948

29442949
# keystone returns endpoint for another account
2945-
fake_ks = FakeKeystone(endpoint='http://example.com:8080/v1/AUTH_bob',
2946-
token='bob_token')
2947-
self.fake_ks_import = _make_fake_import_keystone_client(fake_ks)
2950+
self.fake_ks = FakeKeystone(
2951+
endpoint='http://example.com:8080/v1/AUTH_bob',
2952+
token='bob_token')
29482953

29492954
self.cont = 'c1'
29502955
self.cont_path = '/v1/%s/%s' % (self.account, self.cont)
@@ -3023,8 +3028,7 @@ def test_upload_with_read_write_access(self):
30233028

30243029
args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
30253030
'--leave-segments'])
3026-
with mock.patch('swiftclient.client._import_keystone_client',
3027-
self.fake_ks_import):
3031+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
30283032
with mock.patch('swiftclient.client.http_connection', fake_conn):
30293033
with mock.patch.dict(os.environ, env):
30303034
with CaptureOutput() as out:
@@ -3046,8 +3050,7 @@ def test_upload_with_write_only_access(self):
30463050
on_request=req_handler)
30473051
args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
30483052
'--leave-segments'])
3049-
with mock.patch('swiftclient.client._import_keystone_client',
3050-
self.fake_ks_import):
3053+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
30513054
with mock.patch('swiftclient.client.http_connection', fake_conn):
30523055
with mock.patch.dict(os.environ, env):
30533056
with CaptureOutput() as out:
@@ -3073,8 +3076,7 @@ def test_segment_upload_with_write_only_access(self):
30733076
'--segment-size=10',
30743077
'--segment-container=%s'
30753078
% self.cont])
3076-
with mock.patch('swiftclient.client._import_keystone_client',
3077-
self.fake_ks_import):
3079+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
30783080
with mock.patch('swiftclient.client.http_connection', fake_conn):
30793081
with mock.patch.dict(os.environ, env):
30803082
with CaptureOutput() as out:
@@ -3112,8 +3114,7 @@ def test_segment_upload_with_write_only_access_segments_container(self):
31123114
cmd_args=[self.cont, self.obj,
31133115
'--leave-segments',
31143116
'--segment-size=10'])
3115-
with mock.patch('swiftclient.client._import_keystone_client',
3116-
self.fake_ks_import):
3117+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
31173118
with mock.patch('swiftclient.client.http_connection', fake_conn):
31183119
with mock.patch.dict(os.environ, env):
31193120
with CaptureOutput() as out:
@@ -3149,8 +3150,7 @@ def test_upload_with_no_access(self):
31493150

31503151
args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
31513152
'--leave-segments'])
3152-
with mock.patch('swiftclient.client._import_keystone_client',
3153-
self.fake_ks_import):
3153+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
31543154
with mock.patch('swiftclient.client.http_connection', fake_conn):
31553155
with mock.patch.dict(os.environ, env):
31563156
with CaptureOutput() as out:
@@ -3207,8 +3207,7 @@ def test_download_with_read_write_access(self):
32073207
args, env = self._make_cmd('download', cmd_args=[self.cont,
32083208
self.obj.lstrip('/'),
32093209
'--no-download'])
3210-
with mock.patch('swiftclient.client._import_keystone_client',
3211-
self.fake_ks_import):
3210+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
32123211
with mock.patch('swiftclient.client.http_connection', fake_conn):
32133212
with mock.patch.dict(os.environ, env):
32143213
with CaptureOutput() as out:
@@ -3229,8 +3228,7 @@ def test_download_with_read_only_access(self):
32293228
args, env = self._make_cmd('download', cmd_args=[self.cont,
32303229
self.obj.lstrip('/'),
32313230
'--no-download'])
3232-
with mock.patch('swiftclient.client._import_keystone_client',
3233-
self.fake_ks_import):
3231+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
32343232
with mock.patch('swiftclient.client.http_connection', fake_conn):
32353233
with mock.patch.dict(os.environ, env):
32363234
with CaptureOutput() as out:
@@ -3248,8 +3246,7 @@ def test_download_with_no_access(self):
32483246
args, env = self._make_cmd('download', cmd_args=[self.cont,
32493247
self.obj.lstrip('/'),
32503248
'--no-download'])
3251-
with mock.patch('swiftclient.client._import_keystone_client',
3252-
self.fake_ks_import):
3249+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
32533250
with mock.patch('swiftclient.client.http_connection', fake_conn):
32543251
with mock.patch.dict(os.environ, env):
32553252
with CaptureOutput() as out:
@@ -3273,8 +3270,7 @@ def test_list_with_read_access(self):
32733270
fake_conn = self.fake_http_connection(resp, on_request=req_handler)
32743271

32753272
args, env = self._make_cmd('download', cmd_args=[self.cont])
3276-
with mock.patch('swiftclient.client._import_keystone_client',
3277-
self.fake_ks_import):
3273+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
32783274
with mock.patch('swiftclient.client.http_connection', fake_conn):
32793275
with mock.patch.dict(os.environ, env):
32803276
with CaptureOutput() as out:
@@ -3291,8 +3287,7 @@ def test_list_with_no_access(self):
32913287
fake_conn = self.fake_http_connection(403)
32923288

32933289
args, env = self._make_cmd('download', cmd_args=[self.cont])
3294-
with mock.patch('swiftclient.client._import_keystone_client',
3295-
self.fake_ks_import):
3290+
with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks):
32963291
with mock.patch('swiftclient.client.http_connection', fake_conn):
32973292
with mock.patch.dict(os.environ, env):
32983293
with CaptureOutput() as out:

tests/unit/test_swiftclient.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from requests.exceptions import RequestException
3030

3131
from .utils import (MockHttpTest, fake_get_auth_keystone, StubResponse,
32-
FakeKeystone, _make_fake_import_keystone_client)
32+
FakeKeystone)
3333

3434
from swiftclient.utils import EMPTY_ETAG
3535
from swiftclient.exceptions import ClientException
@@ -322,8 +322,7 @@ def test_auth_v2_timeout(self):
322322
# TestConnection.test_timeout_passed_down but is required to check that
323323
# get_auth does the right thing when it is not passed a timeout arg
324324
fake_ks = FakeKeystone(endpoint='http://some_url', token='secret')
325-
with mock.patch('swiftclient.client._import_keystone_client',
326-
_make_fake_import_keystone_client(fake_ks)):
325+
with mock.patch('swiftclient.client.ksclient_v2', fake_ks):
327326
c.get_auth('http://www.test.com', 'asdf', 'asdf',
328327
os_options=dict(tenant_name='tenant'),
329328
auth_version="2.0", timeout=42.0)
@@ -580,17 +579,15 @@ def test_get_keystone_client_2_0(self):
580579
def test_get_auth_keystone_versionless(self):
581580
fake_ks = FakeKeystone(endpoint='http://some_url', token='secret')
582581

583-
with mock.patch('swiftclient.client._import_keystone_client',
584-
_make_fake_import_keystone_client(fake_ks)):
582+
with mock.patch('swiftclient.client.ksclient_v3', fake_ks):
585583
c.get_auth_keystone('http://authurl', 'user', 'key', {})
586584
self.assertEqual(1, len(fake_ks.calls))
587585
self.assertEqual('http://authurl/v3', fake_ks.calls[0].get('auth_url'))
588586

589587
def test_get_auth_keystone_versionless_auth_version_set(self):
590588
fake_ks = FakeKeystone(endpoint='http://some_url', token='secret')
591589

592-
with mock.patch('swiftclient.client._import_keystone_client',
593-
_make_fake_import_keystone_client(fake_ks)):
590+
with mock.patch('swiftclient.client.ksclient_v2', fake_ks):
594591
c.get_auth_keystone('http://auth_url', 'user', 'key',
595592
{}, auth_version='2.0')
596593
self.assertEqual(1, len(fake_ks.calls))
@@ -600,8 +597,7 @@ def test_get_auth_keystone_versionless_auth_version_set(self):
600597
def test_get_auth_keystone_versionful(self):
601598
fake_ks = FakeKeystone(endpoint='http://some_url', token='secret')
602599

603-
with mock.patch('swiftclient.client._import_keystone_client',
604-
_make_fake_import_keystone_client(fake_ks)):
600+
with mock.patch('swiftclient.client.ksclient_v3', fake_ks):
605601
c.get_auth_keystone('http://auth_url/v3', 'user', 'key',
606602
{}, auth_version='3')
607603
self.assertEqual(1, len(fake_ks.calls))
@@ -611,8 +607,7 @@ def test_get_auth_keystone_versionful(self):
611607
def test_get_auth_keystone_devstack_versionful(self):
612608
fake_ks = FakeKeystone(
613609
endpoint='http://storage.example.com/v1/AUTH_user', token='secret')
614-
with mock.patch('swiftclient.client._import_keystone_client',
615-
_make_fake_import_keystone_client(fake_ks)):
610+
with mock.patch('swiftclient.client.ksclient_v3', fake_ks):
616611
c.get_auth_keystone('https://192.168.8.8/identity/v3',
617612
'user', 'key', {}, auth_version='3')
618613
self.assertEqual(1, len(fake_ks.calls))
@@ -622,8 +617,7 @@ def test_get_auth_keystone_devstack_versionful(self):
622617
def test_get_auth_keystone_devstack_versionless(self):
623618
fake_ks = FakeKeystone(
624619
endpoint='http://storage.example.com/v1/AUTH_user', token='secret')
625-
with mock.patch('swiftclient.client._import_keystone_client',
626-
_make_fake_import_keystone_client(fake_ks)):
620+
with mock.patch('swiftclient.client.ksclient_v3', fake_ks):
627621
c.get_auth_keystone('https://192.168.8.8/identity',
628622
'user', 'key', {}, auth_version='3')
629623
self.assertEqual(1, len(fake_ks.calls))
@@ -634,8 +628,7 @@ def test_auth_keystone_url_some_junk_nonsense(self):
634628
fake_ks = FakeKeystone(
635629
endpoint='http://storage.example.com/v1/AUTH_user',
636630
token='secret')
637-
with mock.patch('swiftclient.client._import_keystone_client',
638-
_make_fake_import_keystone_client(fake_ks)):
631+
with mock.patch('swiftclient.client.ksclient_v3', fake_ks):
639632
c.get_auth_keystone('http://blah.example.com/v2moo',
640633
'user', 'key', {}, auth_version='3')
641634
self.assertEqual(1, len(fake_ks.calls))
@@ -2456,8 +2449,7 @@ def shim_connection(*a, **kw):
24562449
'http://auth.example.com', 'user', 'password', timeout=33.0,
24572450
os_options=os_options, auth_version=2.0)
24582451
fake_ks = FakeKeystone(endpoint='http://some_url', token='secret')
2459-
with mock.patch('swiftclient.client._import_keystone_client',
2460-
_make_fake_import_keystone_client(fake_ks)):
2452+
with mock.patch('swiftclient.client.ksclient_v2', fake_ks):
24612453
with mock.patch.multiple('swiftclient.client',
24622454
http_connection=shim_connection,
24632455
sleep=mock.DEFAULT):

tests/unit/utils.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -542,14 +542,6 @@ class EndpointNotFound(Exception):
542542
pass
543543

544544

545-
def _make_fake_import_keystone_client(fake_import):
546-
def _fake_import_keystone_client(auth_version):
547-
fake_import.auth_version = auth_version
548-
return fake_import, fake_import
549-
550-
return _fake_import_keystone_client
551-
552-
553545
class FakeStream(object):
554546
def __init__(self, size):
555547
self.bytes_read = 0

0 commit comments

Comments
 (0)