Merge "Add transaction id to errors"

This commit is contained in:
Zuul 2024-03-01 20:38:52 +00:00 committed by Gerrit Code Review
commit fa9718deee
7 changed files with 239 additions and 22 deletions

View File

@ -19,10 +19,13 @@ OpenStack Swift client library used internally
import socket import socket
import re import re
import logging import logging
from urllib3.exceptions import HTTPError as urllib_http_error
import warnings import warnings
from requests.exceptions import RequestException, SSLError from requests.exceptions import RequestException, SSLError
import http.client as http_client import http.client as http_client
from requests.structures import CaseInsensitiveDict
from urllib.parse import quote, unquote from urllib.parse import quote, unquote
from urllib.parse import urljoin, urlparse, urlunparse from urllib.parse import urljoin, urlparse, urlunparse
from time import sleep, time from time import sleep, time
@ -287,7 +290,7 @@ class _RetryBody(_ObjectBody):
try: try:
buf = self.resp.read(length) buf = self.resp.read(length)
self.bytes_read += len(buf) self.bytes_read += len(buf)
except (socket.error, RequestException): except (socket.error, urllib_http_error, RequestException):
if self.conn.attempts > self.conn.retries: if self.conn.attempts > self.conn.retries:
raise raise
if (not buf and self.bytes_read < self.expected_length and if (not buf and self.bytes_read < self.expected_length and
@ -735,9 +738,9 @@ def get_auth(auth_url, user, key, **kwargs):
def resp_header_dict(resp): def resp_header_dict(resp):
resp_headers = {} resp_headers = CaseInsensitiveDict()
for header, value in resp.getheaders(): for header, value in resp.getheaders():
header = parse_header_string(header).lower() header = parse_header_string(header)
resp_headers[header] = parse_header_string(value) resp_headers[header] = parse_header_string(value)
return resp_headers return resp_headers
@ -1926,6 +1929,7 @@ class Connection:
is_not_range_request and resp_chunk_size and is_not_range_request and resp_chunk_size and
self.attempts <= self.retries and self.attempts <= self.retries and
rheaders.get('transfer-encoding') is None) rheaders.get('transfer-encoding') is None)
if retry_is_possible: if retry_is_possible:
body = _RetryBody(body.resp, self, container, obj, body = _RetryBody(body.resp, self, container, obj,
resp_chunk_size=resp_chunk_size, resp_chunk_size=resp_chunk_size,

View File

@ -89,6 +89,10 @@ class OutputManager:
msg = msg % fmt_args msg = msg % fmt_args
self.error_print_pool.submit(self._print_error, msg) self.error_print_pool.submit(self._print_error, msg)
def error_with_txn_id(self, swift_err):
self.error("{}\nFailed Transaction ID: {}".format(
swift_err.value, swift_err.transaction_id or 'unknown'))
def get_error_count(self): def get_error_count(self):
return self.error_count return self.error_count

View File

@ -32,6 +32,9 @@ from time import time
from threading import Thread from threading import Thread
from queue import Queue from queue import Queue
from queue import Empty as QueueEmpty from queue import Empty as QueueEmpty
from requests.exceptions import RequestException
from socket import error as socket_error
from urllib3.exceptions import HTTPError as urllib_http_error
from urllib.parse import quote from urllib.parse import quote
import json import json
@ -68,12 +71,16 @@ class ResultsIterator:
class SwiftError(Exception): class SwiftError(Exception):
def __init__(self, value, container=None, obj=None, def __init__(self, value, container=None, obj=None,
segment=None, exc=None): segment=None, exc=None, transaction_id=None):
self.value = value self.value = value
self.container = container self.container = container
self.obj = obj self.obj = obj
self.segment = segment self.segment = segment
self.exception = exc self.exception = exc
if transaction_id is None:
self.transaction_id = getattr(exc, 'transaction_id', None)
else:
self.transaction_id = transaction_id
def __str__(self): def __str__(self):
value = repr(self.value) value = repr(self.value)
@ -459,7 +466,9 @@ class _SwiftReader:
try: try:
self._content_length = int(headers.get('content-length')) self._content_length = int(headers.get('content-length'))
except ValueError: except ValueError:
raise SwiftError('content-length header must be an integer') raise SwiftError(
'content-length header must be an integer',
transaction_id=self._txn_id)
def __iter__(self): def __iter__(self):
for chunk in self._body: for chunk in self._body:
@ -1306,9 +1315,15 @@ class SwiftService:
else: else:
pseudodir = True pseudodir = True
for chunk in obj_body: try:
if fp is not None: for chunk in obj_body:
fp.write(chunk) if fp is not None:
fp.write(chunk)
except (socket_error,
urllib_http_error,
RequestException) as err:
raise ClientException(
str(err), http_response_headers=headers)
finish_time = time() finish_time = time()

View File

@ -227,7 +227,7 @@ def st_delete(parser, args, output_manager, return_parser=False):
output_manager.error('Error Deleting: {0}: {1}' output_manager.error('Error Deleting: {0}: {1}'
.format(p, r['error'])) .format(p, r['error']))
except SwiftError as err: except SwiftError as err:
output_manager.error(err.value) output_manager.error_with_txn_id(err)
st_download_options = '''[--all] [--marker <marker>] [--prefix <prefix>] st_download_options = '''[--all] [--marker <marker>] [--prefix <prefix>]
@ -484,11 +484,13 @@ def st_download(parser, args, output_manager, return_parser=False):
"Object '%s/%s' not found", container, obj) "Object '%s/%s' not found", container, obj)
continue continue
output_manager.error( output_manager.error(
"Error downloading object '%s/%s': %s", "Error downloading object '%s/%s': %s\n"
container, obj, error) "Failed Transaction ID: %s",
container, obj, error,
getattr(error, 'transaction_id', 'unknown'))
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
except Exception as e: except Exception as e:
output_manager.error(e) output_manager.error(e)
@ -670,7 +672,7 @@ def st_list(parser, args, output_manager, return_parser=False):
prt_bytes(totals['bytes'], human)) prt_bytes(totals['bytes'], human))
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_stat_options = '''[--lh] [--header <header:value>] st_stat_options = '''[--lh] [--header <header:value>]
@ -761,7 +763,7 @@ def st_stat(parser, args, output_manager, return_parser=False):
st_stat_options, st_stat_help) st_stat_options, st_stat_help)
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_post_options = '''[--read-acl <acl>] [--write-acl <acl>] [--sync-to <sync-to>] st_post_options = '''[--read-acl <acl>] [--write-acl <acl>] [--sync-to <sync-to>]
@ -867,7 +869,7 @@ def st_post(parser, args, output_manager, return_parser=False):
raise result["error"] raise result["error"]
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_copy_options = '''[--destination </container/object>] [--fresh-metadata] st_copy_options = '''[--destination </container/object>] [--fresh-metadata]
@ -972,7 +974,7 @@ def st_copy(parser, args, output_manager, return_parser=False):
return return
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_upload_options = '''[--changed] [--skip-identical] [--segment-size <size>] st_upload_options = '''[--changed] [--skip-identical] [--segment-size <size>]
@ -1270,7 +1272,7 @@ def st_upload(parser, args, output_manager, return_parser=False):
"to chunk the object") "to chunk the object")
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_capabilities_options = '''[--json] [<proxy_url>] st_capabilities_options = '''[--json] [<proxy_url>]
@ -1332,7 +1334,7 @@ def st_capabilities(parser, args, output_manager, return_parser=False):
del capabilities['swift'] del capabilities['swift']
_print_compo_cap('Additional middleware', capabilities) _print_compo_cap('Additional middleware', capabilities)
except SwiftError as e: except SwiftError as e:
output_manager.error(e.value) output_manager.error_with_txn_id(e)
st_info = st_capabilities st_info = st_capabilities

View File

@ -27,6 +27,7 @@ from unittest import mock
from concurrent.futures import Future from concurrent.futures import Future
from hashlib import md5 from hashlib import md5
from queue import Queue, Empty as QueueEmptyError from queue import Queue, Empty as QueueEmptyError
from requests.structures import CaseInsensitiveDict
from time import sleep from time import sleep
import swiftclient import swiftclient
@ -167,8 +168,10 @@ class TestSwiftReader(unittest.TestCase):
self.assertIsNone(sr._actual_md5) self.assertIsNone(sr._actual_md5)
# Check Contentlength raises error if it isn't an integer # Check Contentlength raises error if it isn't an integer
self.assertRaises(SwiftError, self.sr, 'path', 'body', with self.assertRaises(SwiftError) as cm:
{'content-length': 'notanint'}) self.sr('path', 'body', {'content-length': 'notanint'})
self.assertEqual("'content-length header must be an integer'",
str(cm.exception))
def test_iterator_usage(self): def test_iterator_usage(self):
def _consume(sr): def _consume(sr):
@ -653,6 +656,7 @@ class TestSwiftError(unittest.TestCase):
self.assertIsNone(se.exception) self.assertIsNone(se.exception)
self.assertEqual(str(se), '5') self.assertEqual(str(se), '5')
self.assertNotIn(str(se), 'Transaction ID')
def test_swifterror_creation(self): def test_swifterror_creation(self):
test_exc = Exception('test exc') test_exc = Exception('test exc')
@ -665,6 +669,25 @@ class TestSwiftError(unittest.TestCase):
self.assertEqual(se.exception, test_exc) self.assertEqual(se.exception, test_exc)
self.assertEqual(str(se), '5 container:con object:obj segment:seg') self.assertEqual(str(se), '5 container:con object:obj segment:seg')
self.assertNotIn(str(se), 'Transaction ID')
def test_swifterror_clientexception_creation(self):
test_exc = ClientException(
Exception('test exc'),
http_response_headers=CaseInsensitiveDict({
'x-trans-id': 'someTransId'})
)
se = SwiftError(5, 'con', 'obj', 'seg', test_exc)
self.assertEqual(se.value, 5)
self.assertEqual(se.container, 'con')
self.assertEqual(se.obj, 'obj')
self.assertEqual(se.segment, 'seg')
self.assertEqual(se.exception, test_exc)
self.assertEqual('someTransId', se.transaction_id)
self.assertNotIn('someTransId', str(se))
self.assertIn('5 container:con object:obj segment:seg', str(se))
class TestServiceUtils(unittest.TestCase): class TestServiceUtils(unittest.TestCase):

View File

@ -15,17 +15,21 @@
import io import io
import contextlib import contextlib
import socket
from genericpath import getmtime from genericpath import getmtime
import getpass import getpass
import hashlib import hashlib
import json import json
import logging import logging
import os import os
from requests.structures import CaseInsensitiveDict
import tempfile import tempfile
import unittest import unittest
from unittest import mock from unittest import mock
import textwrap import textwrap
from time import localtime, mktime, strftime, strptime from time import localtime, mktime, strftime, strptime
from requests.exceptions import RequestException
from urllib3.exceptions import HTTPError
import swiftclient import swiftclient
from swiftclient.service import SwiftError from swiftclient.service import SwiftError
@ -235,6 +239,28 @@ class TestShell(unittest.TestCase):
' Sync To: other\n' ' Sync To: other\n'
' Sync Key: secret\n') ' Sync Key: secret\n')
@mock.patch('swiftclient.service.Connection')
def test_stat_container_not_found(self, connection):
connection.return_value.head_container.side_effect = \
swiftclient.ClientException(
'test',
http_status=404,
http_response_headers=CaseInsensitiveDict({
'x-trans-id': 'someTransId'})
)
argv = ["", "stat", "container"]
with CaptureOutput() as output:
with self.assertRaises(SystemExit):
swiftclient.shell.main(argv)
connection.return_value.head_container.assert_called_with(
'container', headers={}, resp_chunk_size=65536,
response_dict={})
self.assertIn('Container \'container\' not found\n'
'Failed Transaction ID: someTransId',
output.err)
@mock.patch('swiftclient.service.Connection') @mock.patch('swiftclient.service.Connection')
def test_stat_container_with_headers(self, connection): def test_stat_container_with_headers(self, connection):
return_headers = { return_headers = {
@ -313,6 +339,27 @@ class TestShell(unittest.TestCase):
' ETag: md5\n' ' ETag: md5\n'
' Manifest: manifest\n') ' Manifest: manifest\n')
@mock.patch('swiftclient.service.Connection')
def test_stat_object_not_found(self, connection):
connection.return_value.head_object.side_effect = \
swiftclient.ClientException(
'test', http_status=404,
http_response_headers=CaseInsensitiveDict({
'x-trans-id': 'someTransId'})
)
argv = ["", "stat", "container", "object"]
with CaptureOutput() as output:
with self.assertRaises(SystemExit):
swiftclient.shell.main(argv)
connection.return_value.head_object.assert_called_with(
'container', 'object', headers={}, resp_chunk_size=65536,
response_dict={})
self.assertIn('test: 404\n'
'Failed Transaction ID: someTransId',
output.err)
@mock.patch('swiftclient.service.Connection') @mock.patch('swiftclient.service.Connection')
def test_stat_object_with_headers(self, connection): def test_stat_object_with_headers(self, connection):
return_headers = { return_headers = {
@ -735,6 +782,117 @@ class TestShell(unittest.TestCase):
swiftclient.shell.main(argv) swiftclient.shell.main(argv)
self.assertEqual('objcontent', output.out) self.assertEqual('objcontent', output.out)
def _do_test_download_clientexception(self, exc):
retry_calls = []
def _fake_retry(conn, *args, **kwargs):
retry_calls.append((args, kwargs))
conn.attempts += 1
body = mock.MagicMock()
body.resp.read.side_effect = RequestException('test_exc')
return (CaseInsensitiveDict({
'content-type': 'text/plain',
'etag': '2cbbfe139a744d6abbe695e17f3c1991',
'x-trans-id': 'someTransId'}),
body)
argv = ["", "download", "container", "object", "--retries", "1"]
with CaptureOutput() as output:
with mock.patch(BUILTIN_OPEN) as mock_open:
with mock.patch("swiftclient.service.Connection._retry",
_fake_retry):
with self.assertRaises(SystemExit):
swiftclient.shell.main(argv)
mock_open.assert_called_with('object', 'wb', 65536)
self.assertEqual([
((None, swiftclient.client.get_object, 'container', 'object'),
{'headers': {},
'query_string': None,
'resp_chunk_size': 65536,
'response_dict': {}}),
((None, swiftclient.client.get_object, 'container', 'object'),
{'attempts': 1,
'headers': {'If-Match': mock.ANY, 'Range': 'bytes=0-'},
'query_string': None,
'resp_chunk_size': 65536,
'response_dict': {}})],
retry_calls)
self.assertIn('Error downloading object \'container/object\': '
'test_exc',
str(output.err))
self.assertIn('someTransId', str(output.err))
def test_download_request_exception(self):
self._do_test_download_clientexception(RequestException('text_exc'))
def test_download_socket_error(self):
self._do_test_download_clientexception(socket.error())
def test_download_http_error(self):
self._do_test_download_clientexception(HTTPError)
def test_download_request_exception_retries_0(self):
retry_calls = []
def _fake_retry(conn, *args, **kwargs):
retry_calls.append((args, kwargs))
conn.attempts += 1
body = mock.MagicMock()
body.__iter__.side_effect = RequestException('test_exc')
return (CaseInsensitiveDict({
'content-type': 'text/plain',
'etag': '2cbbfe139a744d6abbe695e17f3c1991',
'x-trans-id': 'someTransId'}),
body)
argv = ["", "download", "container", "object", "--retries", "0"]
with CaptureOutput() as output:
with mock.patch(BUILTIN_OPEN) as mock_open:
with mock.patch("swiftclient.service.Connection._retry",
_fake_retry):
with self.assertRaises(SystemExit):
swiftclient.shell.main(argv)
mock_open.assert_called_with('object', 'wb', 65536)
self.assertEqual([
((None, swiftclient.client.get_object, 'container', 'object'),
{'headers': {},
'query_string': None,
'resp_chunk_size': 65536,
'response_dict': {}}), ],
retry_calls)
self.assertIn('Error downloading object \'container/object\': '
'test_exc',
str(output.err))
self.assertIn('someTransId', str(output.err))
@mock.patch('swiftclient.service.Connection')
def test_download_bad_content_length(self, connection):
objcontent = io.BytesIO(b'objcontent')
connection.return_value.get_object.side_effect = [
(CaseInsensitiveDict({
'content-type': 'text/plain',
'content-length': 'BAD',
'etag': '2cbbfe139a744d6abbe695e17f3c1991',
'x-trans-id': 'someTransId'}),
objcontent)
]
with CaptureOutput() as output:
with self.assertRaises(SystemExit):
with mock.patch(BUILTIN_OPEN) as mock_open:
argv = ["", "download", "container", "object"]
swiftclient.shell.main(argv)
connection.return_value.get_object.assert_called_with(
'container', 'object', headers={}, resp_chunk_size=65536,
response_dict={})
mock_open.assert_called_with('object', 'wb', 65536)
self.assertIn("Error downloading object \'container/object\': "
"'content-length header must be an integer'"
"\nFailed Transaction ID: someTransId",
str(output.err))
@mock.patch('swiftclient.service.shuffle') @mock.patch('swiftclient.service.shuffle')
@mock.patch('swiftclient.service.Connection') @mock.patch('swiftclient.service.Connection')
def test_download_shuffle(self, connection, mock_shuffle): def test_download_shuffle(self, connection, mock_shuffle):
@ -1930,7 +2088,9 @@ class TestShell(unittest.TestCase):
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
swiftclient.shell.main(argv) swiftclient.shell.main(argv)
self.assertEqual(output.err, 'Account not found\n') self.assertEqual(
output.err,
'Account not found\nFailed Transaction ID: unknown\n')
@mock.patch('swiftclient.service.Connection') @mock.patch('swiftclient.service.Connection')
def test_post_container(self, connection): def test_post_container(self, connection):
@ -2122,7 +2282,8 @@ class TestShell(unittest.TestCase):
self.assertEqual( self.assertEqual(
output.err, output.err,
'Combination of multiple objects and destination ' 'Combination of multiple objects and destination '
'including object is invalid\n') 'including object is invalid\n'
'Failed Transaction ID: unknown\n')
@mock.patch('swiftclient.service.Connection') @mock.patch('swiftclient.service.Connection')
def test_copy_object_bad_auth(self, connection): def test_copy_object_bad_auth(self, connection):

View File

@ -1118,6 +1118,14 @@ class TestGetObject(MockHttpTest):
self.assertEqual('%ff', headers.get('x-non-utf-8-header', '')) self.assertEqual('%ff', headers.get('x-non-utf-8-header', ''))
self.assertEqual('%FF', headers.get('x-binary-header', '')) self.assertEqual('%FF', headers.get('x-binary-header', ''))
self.assertEqual('t\xe9st', headers.get('X-Utf-8-Header', ''))
self.assertEqual('%ff', headers.get('X-Non-Utf-8-Header', ''))
self.assertEqual('%FF', headers.get('X-Binary-Header', ''))
self.assertEqual('t\xe9st', headers.get('X-UTF-8-HEADER', ''))
self.assertEqual('%ff', headers.get('X-NON-UTF-8-HEADER', ''))
self.assertEqual('%FF', headers.get('X-BINARY-HEADER', ''))
def test_chunk_size_read_method(self): def test_chunk_size_read_method(self):
conn = c.Connection('http://auth.url/', 'some_user', 'some_key') conn = c.Connection('http://auth.url/', 'some_user', 'some_key')
with mock.patch('swiftclient.client.get_auth_1_0') as mock_get_auth: with mock.patch('swiftclient.client.get_auth_1_0') as mock_get_auth: