diff options
-rw-r--r-- | doc/source/swiftclient.rst | 5 | ||||
-rw-r--r-- | swiftclient/client.py | 9 | ||||
-rwxr-xr-x | swiftclient/shell.py | 4 | ||||
-rw-r--r-- | swiftclient/utils.py | 14 | ||||
-rw-r--r-- | test/unit/test_shell.py | 21 | ||||
-rw-r--r-- | test/unit/test_swiftclient.py | 58 |
6 files changed, 106 insertions, 5 deletions
diff --git a/doc/source/swiftclient.rst b/doc/source/swiftclient.rst index 108443a..b352b1d 100644 --- a/doc/source/swiftclient.rst +++ b/doc/source/swiftclient.rst @@ -4,6 +4,7 @@ swiftclient ============== .. automodule:: swiftclient + :inherited-members: swiftclient.authv1 ================== @@ -15,16 +16,19 @@ swiftclient.client ================== .. automodule:: swiftclient.client + :inherited-members: swiftclient.service =================== .. automodule:: swiftclient.service + :inherited-members: swiftclient.exceptions ====================== .. automodule:: swiftclient.exceptions + :inherited-members: swiftclient.multithreading ========================== @@ -35,3 +39,4 @@ swiftclient.utils ================= .. automodule:: swiftclient.utils + :inherited-members: diff --git a/swiftclient/client.py b/swiftclient/client.py index b9f12aa..0635090 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -1798,8 +1798,13 @@ class Connection: service_token=self.service_token, **kwargs) self._add_response_dict(caller_response_dict, kwargs) return rv - except SSLError: - raise + except SSLError as e: + self._add_response_dict(caller_response_dict, kwargs) + if ('certificate verify' in str(e)) or \ + ('hostname' in str(e)) or \ + self.attempts > self.retries: + raise + self.http_conn = None except (socket.error, RequestException): self._add_response_dict(caller_response_dict, kwargs) if self.attempts > self.retries: diff --git a/swiftclient/shell.py b/swiftclient/shell.py index 319141d..69f9481 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -31,7 +31,7 @@ from time import gmtime, strftime from swiftclient import RequestException from swiftclient.utils import config_true_value, generate_temp_url, \ - prt_bytes, JSONableIterable + prt_bytes, parse_timeout, JSONableIterable from swiftclient.multithreading import OutputManager from swiftclient.exceptions import ClientException from swiftclient import __version__ as client_version @@ -1752,7 +1752,7 @@ def add_default_args(parser): parser.add_argument('-K', '--key', dest='key', default=environ.get('ST_KEY'), help='Key for obtaining an auth token.') - parser.add_argument('-T', '--timeout', type=int, dest='timeout', + parser.add_argument('-T', '--timeout', type=parse_timeout, dest='timeout', default=None, help='Timeout in seconds to wait for response.') parser.add_argument('-R', '--retries', type=int, default=5, dest='retries', diff --git a/swiftclient/utils.py b/swiftclient/utils.py index 0a67537..39481e4 100644 --- a/swiftclient/utils.py +++ b/swiftclient/utils.py @@ -70,6 +70,20 @@ def prt_bytes(num_bytes, human_flag): return '%.1f%s' % (num, suffix) +def parse_timeout(value): + for suffix, multiplier in ( + ('s', 1), + ('m', 60), + ('min', 60), + ('h', 60 * 60), + ('hr', 60 * 60), + ('d', 24 * 60 * 60), + ): + if value.endswith(suffix): + return multiplier * float(value[:-len(suffix)]) + return float(value) + + def parse_timestamp(seconds, absolute=False): try: try: diff --git a/test/unit/test_shell.py b/test/unit/test_shell.py index d494167..5e69f4a 100644 --- a/test/unit/test_shell.py +++ b/test/unit/test_shell.py @@ -2481,6 +2481,27 @@ class TestDebugAndInfoOptions(unittest.TestCase): % (mock_logging.call_args_list, argv)) +@mock.patch.dict(os.environ, mocked_os_environ) +class TestTimeoutOption(unittest.TestCase): + @mock.patch('swiftclient.service.Connection') + def test_timeout_parsing(self, connection): + for timeout, expected in ( + ("12", 12), + ("12.3", 12.3), + ("5s", 5), + ("25.6s", 25.6), + ("2m", 120), + ("2.5min", 150), + ("1h", 3600), + (".5hr", 1800), + ): + connection.reset_mock() + with self.subTest(timeout=timeout): + swiftclient.shell.main(["", "stat", "--timeout", timeout]) + self.assertEqual(connection.mock_calls[0].kwargs['timeout'], + expected) + + class TestBase(unittest.TestCase): """ Provide some common methods to subclasses diff --git a/test/unit/test_swiftclient.py b/test/unit/test_swiftclient.py index 55b4679..42d470c 100644 --- a/test/unit/test_swiftclient.py +++ b/test/unit/test_swiftclient.py @@ -25,7 +25,7 @@ import warnings import tempfile from hashlib import md5 from urllib.parse import urlparse -from requests.exceptions import RequestException +from requests.exceptions import RequestException, SSLError from .utils import (MockHttpTest, fake_get_auth_keystone, StubResponse, FakeKeystone) @@ -2176,6 +2176,62 @@ class TestConnection(MockHttpTest): self.assertEqual(mock_auth.call_count, 1) self.assertEqual(conn.attempts, conn.retries + 1) + def test_no_retry_with_cert_sslerror(self): + def quick_sleep(*args): + pass + c.sleep = quick_sleep + for err in ( + # Taken from real testing (requests==2.25.1, urllib3==1.26.5, + # pyOpenSSL==21.0.0) but note that these are actually way more + # messy/wrapped up in other exceptions + SSLError( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'certificate has expired (_ssl.c:997)'), + SSLError( + "hostname 'wrong.host.badssl.com' doesn't match either of " + "'*.badssl.com', 'badssl.com'"), + SSLError( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'self-signed certificate (_ssl.c:997)'), + SSLError( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'self-signed certificate in certificate chain (_ssl.c:997)'), + SSLError( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'unable to get local issuer certificate (_ssl.c:997)'), + SSLError( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'CA signature digest algorithm too weak (_ssl.c:997)'), + ): + conn = c.Connection('http://www.test.com', 'asdf', 'asdf') + with self.subTest(err=err), 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 = err + self.assertRaises(socket.error, conn.head_account) + self.assertEqual(mock_auth.call_count, 1) + self.assertEqual(conn.attempts, 1) + + def test_retry_with_non_cert_sslerror(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 = SSLError( + "HTTPSConnectionPool(host='example.com', port=443): " + "Max retries exceeded with url: /v1/AUTH_test (Caused by " + "SSLError(SSLZeroReturnError(6, 'TLS/SSL connection has " + "been closed (EOF) (_ssl.c:997)')))") + 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 |