summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/swiftclient.rst5
-rw-r--r--swiftclient/client.py9
-rwxr-xr-xswiftclient/shell.py4
-rw-r--r--swiftclient/utils.py14
-rw-r--r--test/unit/test_shell.py21
-rw-r--r--test/unit/test_swiftclient.py58
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