diff options
-rw-r--r-- | swiftclient/client.py | 9 | ||||
-rw-r--r-- | swiftclient/service.py | 4 | ||||
-rwxr-xr-x | swiftclient/shell.py | 6 | ||||
-rw-r--r-- | tests/unit/test_swiftclient.py | 67 |
4 files changed, 84 insertions, 2 deletions
diff --git a/swiftclient/client.py b/swiftclient/client.py index 60abbd8..ab0fde8 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -1542,7 +1542,7 @@ class Connection(object): os_options=None, auth_version="1", cacert=None, insecure=False, cert=None, cert_key=None, ssl_compression=True, retry_on_ratelimit=False, - timeout=None, session=None): + timeout=None, session=None, force_auth_retry=False): """ :param authurl: authentication URL :param user: user name to authenticate as @@ -1578,6 +1578,8 @@ class Connection(object): after a backoff. :param timeout: The connect timeout for the HTTP connection. :param session: A keystoneauth session object. + :param force_auth_retry: reset auth info even if client got unexpected + error except 401 Unauthorized. """ self.session = session self.authurl = authurl @@ -1610,6 +1612,7 @@ class Connection(object): self.auth_end_time = 0 self.retry_on_ratelimit = retry_on_ratelimit self.timeout = timeout + self.force_auth_retry = force_auth_retry def close(self): if (self.http_conn and isinstance(self.http_conn, tuple) @@ -1724,6 +1727,10 @@ class Connection(object): pass else: raise + + if self.force_auth_retry: + self.url = self.token = self.service_token = None + sleep(backoff) backoff = min(backoff * 2, self.max_backoff) if reset_func: diff --git a/swiftclient/service.py b/swiftclient/service.py index ed5e9e9..0679fec 100644 --- a/swiftclient/service.py +++ b/swiftclient/service.py @@ -144,6 +144,7 @@ def _build_default_global_options(): "user": environ.get('ST_USER'), "key": environ.get('ST_KEY'), "retries": 5, + "force_auth_retry": False, "os_username": environ.get('OS_USERNAME'), "os_user_id": environ.get('OS_USER_ID'), "os_user_domain_name": environ.get('OS_USER_DOMAIN_NAME'), @@ -261,7 +262,8 @@ def get_conn(options): insecure=options['insecure'], cert=options['os_cert'], cert_key=options['os_key'], - ssl_compression=options['ssl_compression']) + ssl_compression=options['ssl_compression'], + force_auth_retry=options['force_auth_retry']) def mkdirs(path): diff --git a/swiftclient/shell.py b/swiftclient/shell.py index d02c709..6010b5d 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -1501,6 +1501,7 @@ def main(arguments=None): [--os-cert <client-certificate-file>] [--os-key <client-certificate-key-file>] [--no-ssl-compression] + [--force-auth-retry] <subcommand> [--help] [<subcommand options>] Command-line interface to the OpenStack Swift API. @@ -1610,6 +1611,11 @@ Examples: help='This option is deprecated and not used anymore. ' 'SSL compression should be disabled by default ' 'by the system SSL library.') + parser.add_argument('--force-auth-retry', + action='store_true', dest='force_auth_retry', + default=False, + help='Force a re-auth attempt on ' + 'any error other than 401 unauthorized') os_grp = parser.add_argument_group("OpenStack authentication options") os_grp.add_argument('--os-username', diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py index 7b8628b..52153dc 100644 --- a/tests/unit/test_swiftclient.py +++ b/tests/unit/test_swiftclient.py @@ -26,11 +26,13 @@ import tempfile from hashlib import md5 from six import binary_type from six.moves.urllib.parse import urlparse +from requests.exceptions import RequestException from .utils import (MockHttpTest, fake_get_auth_keystone, StubResponse, FakeKeystone, _make_fake_import_keystone_client) from swiftclient.utils import EMPTY_ETAG +from swiftclient.exceptions import ClientException from swiftclient import client as c import swiftclient.utils import swiftclient @@ -1978,6 +1980,71 @@ class TestConnection(MockHttpTest): self.assertIn('Account HEAD failed', str(exc_context.exception)) self.assertEqual(conn.attempts, 1) + def test_retry_with_socket_error(self): + def quick_sleep(*args): + pass + c.sleep = quick_sleep + conn = c.Connection('http://www.test.com', 'asdf', 'asdf') + with mock.patch('swiftclient.client.http_connection') as \ + fake_http_connection, \ + mock.patch('swiftclient.client.get_auth_1_0') as mock_auth: + mock_auth.return_value = ('http://mock.com', 'mock_token') + fake_http_connection.side_effect = socket.error + self.assertRaises(socket.error, conn.head_account) + self.assertEqual(mock_auth.call_count, 1) + self.assertEqual(conn.attempts, conn.retries + 1) + + def test_retry_with_force_auth_retry_exceptions(self): + def quick_sleep(*args): + pass + + def do_test(exception): + c.sleep = quick_sleep + conn = c.Connection( + 'http://www.test.com', 'asdf', 'asdf', + force_auth_retry=True) + with mock.patch('swiftclient.client.http_connection') as \ + fake_http_connection, \ + mock.patch('swiftclient.client.get_auth_1_0') as mock_auth: + mock_auth.return_value = ('http://mock.com', 'mock_token') + fake_http_connection.side_effect = exception + self.assertRaises(exception, conn.head_account) + self.assertEqual(mock_auth.call_count, conn.retries + 1) + self.assertEqual(conn.attempts, conn.retries + 1) + + do_test(socket.error) + do_test(RequestException) + + def test_retry_with_force_auth_retry_client_exceptions(self): + def quick_sleep(*args): + pass + + def do_test(http_status, count): + + def mock_http_connection(*args, **kwargs): + raise ClientException('fake', http_status=http_status) + + c.sleep = quick_sleep + conn = c.Connection( + 'http://www.test.com', 'asdf', 'asdf', + force_auth_retry=True) + with mock.patch('swiftclient.client.http_connection') as \ + fake_http_connection, \ + mock.patch('swiftclient.client.get_auth_1_0') as mock_auth: + mock_auth.return_value = ('http://mock.com', 'mock_token') + fake_http_connection.side_effect = mock_http_connection + self.assertRaises(ClientException, conn.head_account) + self.assertEqual(mock_auth.call_count, count) + self.assertEqual(conn.attempts, count) + + # sanity, in case of 401, the auth will be called only twice because of + # retried_auth mechanism + do_test(401, 2) + # others will be tried until retry limits + do_test(408, 6) + do_test(500, 6) + do_test(503, 6) + def test_resp_read_on_server_error(self): conn = c.Connection('http://www.test.com', 'asdf', 'asdf', retries=0) |