Merge lp:~gholt/swift/lobjects4 into lp:~hudson-openstack/swift/trunk

Proposed by gholt
Status: Superseded
Proposed branch: lp:~gholt/swift/lobjects4
Merge into: lp:~hudson-openstack/swift/trunk
Diff against target: 2282 lines (+1404/-78)
11 files modified
bin/st (+180/-39)
doc/source/index.rst (+1/-0)
doc/source/overview_large_objects.rst (+177/-0)
swift/common/client.py (+17/-10)
swift/common/constraints.py (+11/-0)
swift/obj/server.py (+7/-2)
swift/proxy/server.py (+265/-3)
test/functionalnosetests/test_object.py (+279/-0)
test/unit/common/test_constraints.py (+27/-0)
test/unit/obj/test_server.py (+82/-22)
test/unit/proxy/test_server.py (+358/-2)
To merge this branch: bzr merge lp:~gholt/swift/lobjects4
Reviewer Review Type Date Requested Status
John Dickinson Approve
Greg Lange (community) Approve
gholt (community) Approve
clayg Approve
Review via email: mp+41020@code.launchpad.net

This proposal has been superseded by a proposal from 2010-12-14.

Commit message

Large object support by allowing the client to upload the object in segments and download them all as a single object.

Description of the change

Added large object support by allowing the client to upload the object in segments and download them all as a single object. Also, made updates client.py and st to support and provide an example of how to use the feature. Finally, there is an overview document that needs reviewing.

To post a comment you must log in.
lp:~gholt/swift/lobjects4 updated
134. By gholt

Merged from trunk

135. By gholt

Added history section for large object support docs

136. By gholt

Fixed bug dfg found in st

Revision history for this message
clayg (clay-gerrard) wrote :

great work gholt.

In SegmentedIterable, _load_next_segment, __iter__ and app_iter_range all log the same message ("ERROR: While processing manifest") - if the exception happens in _load_next_segment it gets logged twice.

Eitherway, the posthooklogger will log the status of the response as successful, even though the client likely blew up cause it didn't get all the expected data.

Just for the purpose of logging, since SegmentedIterable has a backref to the Response - I would suggest we update the status_int to a 503. Even though we can't do anything about the 200 we already sent to the client, we know that whatever bytes were transferred so far won't be billable.

Just a thought - personal preference in control structure - I don't care for the except Exception: if not isisntance(StopIteration): log, raise - I would prefer except StopIteration: pass except Exception: log, raise.

Also in SegmentedIterable, instead of initializing self.response to None, how about an empty Response(). It *should* be overridden (assuming the caller read the docstring) - but initializing to "something" removes the need for an extra test in all the "if self.response: update attribute on response" code. Alternatively you could make response a required named parameter to __init__ and then just require the calling method update it's response's app_iter instead of its new SegmentedIterable's backref.

resp = Response()
resp.app_iter = SegIter(resp)

^ does that work?

The history section mentions the "implied user manifest" that we have now with the consistency issue where the proxy can't ever know if it has all of the users intended objects, but not the rejected "explicit user manifest" when the body of the manifest file includes all of the objects in the order the should be glued together - like amazon uses. Is it worth mentioning why our solution is better?

Thanks for working on this essential feature, you deserve much karama!

Revision history for this message
gholt (gholt) wrote :

Cool, will apply your stuff tomorrow. The except Exception: if not isinstance was pure lame on my part. The other stuff you already know I like. :) I dunno on the history doc thing; wanna type up what you'd like to see? Amazon's thing is really quite different since with them you still can't exceed the maximum 5G limit they have, glued or not.

Revision history for this message
gholt (gholt) wrote :

Eh, I was bored so I just pushed the changes described.

lp:~gholt/swift/lobjects4 updated
137. By gholt

Merged from trunk

138. By gholt

SegmentIterable: logs exceptions just once; 503s on exception; fix except syntax; make sure self.response is always *something*

Revision history for this message
clayg (clay-gerrard) wrote :

I think this is ready!

I put up an ether on the large objects history section if anyone wants to suggest any changes: http://etherpad.openstack.org/SwiftLargeObjectSupport

review: Approve
lp:~gholt/swift/lobjects4 updated
139. By gholt

Merge from trunk

140. By gholt

Merge from lp:~clay-gerrard/swift/lobjects_history

Revision history for this message
gholt (gholt) wrote :

I'm not sure how I ended up a reviewer on my own branch; but I approve!

review: Approve
lp:~gholt/swift/lobjects4 updated
141. By gholt

Merged from trunk

Revision history for this message
Greg Lange (greglange) wrote :

I like the change. My only gripe would be that an explicit handling of the 10,000 segment limit would be better, either allow unlimited segments or error when more than 10,000 segments are uploaded as appropriate. This is being addressed in a bug report.

review: Approve
Revision history for this message
John Dickinson (notmyname) wrote :

looks great

review: Approve
lp:~gholt/swift/lobjects4 updated
142. By gholt

Now supports infinite objects!

143. By gholt

Fixed a bug where a HEAD on a really, really large object would give a content-length of 0 instead of transfer-encoding: chunked

144. By gholt

x-copy-from now understands manifest sources and copies details rather than contents

145. By gholt

Limit manifest gets to one segment per second; prevents amplification attacks of tons of tiny segments

146. By gholt

Changed to only limit manifest gets after first 10 segments. Makes tests run faster but does allow amplification 1:10. At least it's not 1:infinity like before.

147. By gholt

Even though isn't 100% related, made st emit a warning if there's a / in a container name

148. By gholt

st: Works with chunked transfer encoded downloads now

149. By gholt

Made stat display of objects suppress content-length, last-modified, and etag if they aren't in the headers

150. By gholt

lobjects: The Last-Modified header is now determined for reasonably segmented objects.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bin/st'
--- bin/st 2010-12-09 17:10:37 +0000
+++ bin/st 2010-12-13 22:17:44 +0000
@@ -581,7 +581,8 @@
581 :param container: container name that the object is in581 :param container: container name that the object is in
582 :param name: object name to put582 :param name: object name to put
583 :param contents: a string or a file like object to read object data from583 :param contents: a string or a file like object to read object data from
584 :param content_length: value to send as content-length header584 :param content_length: value to send as content-length header; also limits
585 the amount read from contents
585 :param etag: etag of contents586 :param etag: etag of contents
586 :param chunk_size: chunk size of data to write587 :param chunk_size: chunk size of data to write
587 :param content_type: value to send as content-type header588 :param content_type: value to send as content-type header
@@ -611,18 +612,24 @@
611 conn.putrequest('PUT', path)612 conn.putrequest('PUT', path)
612 for header, value in headers.iteritems():613 for header, value in headers.iteritems():
613 conn.putheader(header, value)614 conn.putheader(header, value)
614 if not content_length:615 if content_length is None:
615 conn.putheader('Transfer-Encoding', 'chunked')616 conn.putheader('Transfer-Encoding', 'chunked')
616 conn.endheaders()617 conn.endheaders()
617 chunk = contents.read(chunk_size)618 chunk = contents.read(chunk_size)
618 while chunk:619 while chunk:
619 if not content_length:
620 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))620 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
621 else:621 chunk = contents.read(chunk_size)
622 conn.send('0\r\n\r\n')
623 else:
624 conn.endheaders()
625 left = content_length
626 while left > 0:
627 size = chunk_size
628 if size > left:
629 size = left
630 chunk = contents.read(size)
622 conn.send(chunk)631 conn.send(chunk)
623 chunk = contents.read(chunk_size)632 left -= len(chunk)
624 if not content_length:
625 conn.send('0\r\n\r\n')
626 else:633 else:
627 conn.request('PUT', path, contents, headers)634 conn.request('PUT', path, contents, headers)
628 resp = conn.getresponse()635 resp = conn.getresponse()
@@ -860,15 +867,20 @@
860867
861868
862st_delete_help = '''869st_delete_help = '''
863delete --all OR delete container [object] [object] ...870delete --all OR delete container [--leave-segments] [object] [object] ...
864 Deletes everything in the account (with --all), or everything in a871 Deletes everything in the account (with --all), or everything in a
865 container, or a list of objects depending on the args given.'''.strip('\n')872 container, or a list of objects depending on the args given. Segments of
873 manifest objects will be deleted as well, unless you specify the
874 --leave-segments option.'''.strip('\n')
866875
867876
868def st_delete(parser, args, print_queue, error_queue):877def st_delete(parser, args, print_queue, error_queue):
869 parser.add_option('-a', '--all', action='store_true', dest='yes_all',878 parser.add_option('-a', '--all', action='store_true', dest='yes_all',
870 default=False, help='Indicates that you really want to delete '879 default=False, help='Indicates that you really want to delete '
871 'everything in the account')880 'everything in the account')
881 parser.add_option('', '--leave-segments', action='store_true',
882 dest='leave_segments', default=False, help='Indicates that you want '
883 'the segments of manifest objects left alone')
872 (options, args) = parse_args(parser, args)884 (options, args) = parse_args(parser, args)
873 args = args[1:]885 args = args[1:]
874 if (not args and not options.yes_all) or (args and options.yes_all):886 if (not args and not options.yes_all) or (args and options.yes_all):
@@ -876,11 +888,42 @@
876 (basename(argv[0]), st_delete_help))888 (basename(argv[0]), st_delete_help))
877 return889 return
878890
891 def _delete_segment((container, obj), conn):
892 conn.delete_object(container, obj)
893 if options.verbose:
894 print_queue.put('%s/%s' % (container, obj))
895
879 object_queue = Queue(10000)896 object_queue = Queue(10000)
880897
881 def _delete_object((container, obj), conn):898 def _delete_object((container, obj), conn):
882 try:899 try:
900 old_manifest = None
901 if not options.leave_segments:
902 try:
903 old_manifest = conn.head_object(container, obj).get(
904 'x-object-manifest')
905 except ClientException, err:
906 if err.http_status != 404:
907 raise
883 conn.delete_object(container, obj)908 conn.delete_object(container, obj)
909 if old_manifest:
910 segment_queue = Queue(10000)
911 scontainer, sprefix = old_manifest.split('/', 1)
912 for delobj in conn.get_container(scontainer,
913 prefix=sprefix)[1]:
914 segment_queue.put((scontainer, delobj['name']))
915 if not segment_queue.empty():
916 segment_threads = [QueueFunctionThread(segment_queue,
917 _delete_segment, create_connection()) for _ in
918 xrange(10)]
919 for thread in segment_threads:
920 thread.start()
921 while not segment_queue.empty():
922 sleep(0.01)
923 for thread in segment_threads:
924 thread.abort = True
925 while thread.isAlive():
926 thread.join(0.01)
884 if options.verbose:927 if options.verbose:
885 path = options.yes_all and join(container, obj) or obj928 path = options.yes_all and join(container, obj) or obj
886 if path[:1] in ('/', '\\'):929 if path[:1] in ('/', '\\'):
@@ -891,6 +934,7 @@
891 raise934 raise
892 error_queue.put('Object %s not found' %935 error_queue.put('Object %s not found' %
893 repr('%s/%s' % (container, obj)))936 repr('%s/%s' % (container, obj)))
937
894 container_queue = Queue(10000)938 container_queue = Queue(10000)
895939
896 def _delete_container(container, conn):940 def _delete_container(container, conn):
@@ -976,7 +1020,7 @@
9761020
9771021
978st_download_help = '''1022st_download_help = '''
979download --all OR download container [object] [object] ...1023download --all OR download container [options] [object] [object] ...
980 Downloads everything in the account (with --all), or everything in a1024 Downloads everything in the account (with --all), or everything in a
981 container, or a list of objects depending on the args given. For a single1025 container, or a list of objects depending on the args given. For a single
982 object download, you may use the -o [--output] <filename> option to1026 object download, you may use the -o [--output] <filename> option to
@@ -1020,15 +1064,18 @@
1020 path = options.yes_all and join(container, obj) or obj1064 path = options.yes_all and join(container, obj) or obj
1021 if path[:1] in ('/', '\\'):1065 if path[:1] in ('/', '\\'):
1022 path = path[1:]1066 path = path[1:]
1067 md5sum = None
1023 make_dir = out_file != "-"1068 make_dir = out_file != "-"
1024 if content_type.split(';', 1)[0] == 'text/directory':1069 if content_type.split(';', 1)[0] == 'text/directory':
1025 if make_dir and not isdir(path):1070 if make_dir and not isdir(path):
1026 mkdirs(path)1071 mkdirs(path)
1027 read_length = 01072 read_length = 0
1028 md5sum = md5()1073 if 'x-object-manifest' not in headers:
1074 md5sum = md5()
1029 for chunk in body:1075 for chunk in body:
1030 read_length += len(chunk)1076 read_length += len(chunk)
1031 md5sum.update(chunk)1077 if md5sum:
1078 md5sum.update(chunk)
1032 else:1079 else:
1033 dirpath = dirname(path)1080 dirpath = dirname(path)
1034 if make_dir and dirpath and not isdir(dirpath):1081 if make_dir and dirpath and not isdir(dirpath):
@@ -1040,13 +1087,15 @@
1040 else:1087 else:
1041 fp = open(path, 'wb')1088 fp = open(path, 'wb')
1042 read_length = 01089 read_length = 0
1043 md5sum = md5()1090 if 'x-object-manifest' not in headers:
1091 md5sum = md5()
1044 for chunk in body:1092 for chunk in body:
1045 fp.write(chunk)1093 fp.write(chunk)
1046 read_length += len(chunk)1094 read_length += len(chunk)
1047 md5sum.update(chunk)1095 if md5sum:
1096 md5sum.update(chunk)
1048 fp.close()1097 fp.close()
1049 if md5sum.hexdigest() != etag:1098 if md5sum and md5sum.hexdigest() != etag:
1050 error_queue.put('%s: md5sum != etag, %s != %s' %1099 error_queue.put('%s: md5sum != etag, %s != %s' %
1051 (path, md5sum.hexdigest(), etag))1100 (path, md5sum.hexdigest(), etag))
1052 if read_length != content_length:1101 if read_length != content_length:
@@ -1267,6 +1316,9 @@
1267 headers.get('content-length'),1316 headers.get('content-length'),
1268 headers.get('last-modified'),1317 headers.get('last-modified'),
1269 headers.get('etag')))1318 headers.get('etag')))
1319 if 'x-object-manifest' in headers:
1320 print_queue.put(' Manifest: %s' %
1321 headers['x-object-manifest'])
1270 for key, value in headers.items():1322 for key, value in headers.items():
1271 if key.startswith('x-object-meta-'):1323 if key.startswith('x-object-meta-'):
1272 print_queue.put('%14s: %s' % ('Meta %s' %1324 print_queue.put('%14s: %s' % ('Meta %s' %
@@ -1274,7 +1326,7 @@
1274 for key, value in headers.items():1326 for key, value in headers.items():
1275 if not key.startswith('x-object-meta-') and key not in (1327 if not key.startswith('x-object-meta-') and key not in (
1276 'content-type', 'content-length', 'last-modified',1328 'content-type', 'content-length', 'last-modified',
1277 'etag', 'date'):1329 'etag', 'date', 'x-object-manifest'):
1278 print_queue.put(1330 print_queue.put(
1279 '%14s: %s' % (key.title(), value))1331 '%14s: %s' % (key.title(), value))
1280 except ClientException, err:1332 except ClientException, err:
@@ -1363,23 +1415,48 @@
1363upload [options] container file_or_directory [file_or_directory] [...]1415upload [options] container file_or_directory [file_or_directory] [...]
1364 Uploads to the given container the files and directories specified by the1416 Uploads to the given container the files and directories specified by the
1365 remaining args. -c or --changed is an option that will only upload files1417 remaining args. -c or --changed is an option that will only upload files
1366 that have changed since the last upload.'''.strip('\n')1418 that have changed since the last upload. -S <size> or --segment-size <size>
1419 and --leave-segments are options as well (see --help for more).
1420'''.strip('\n')
13671421
13681422
1369def st_upload(options, args, print_queue, error_queue):1423def st_upload(options, args, print_queue, error_queue):
1370 parser.add_option('-c', '--changed', action='store_true', dest='changed',1424 parser.add_option('-c', '--changed', action='store_true', dest='changed',
1371 default=False, help='Will only upload files that have changed since '1425 default=False, help='Will only upload files that have changed since '
1372 'the last upload')1426 'the last upload')
1427 parser.add_option('-S', '--segment-size', dest='segment_size', help='Will '
1428 'upload files in segments no larger than <size> and then create a '
1429 '"manifest" file that will download all the segments as if it were '
1430 'the original file. The segments will be uploaded to a '
1431 '<container>_segments container so as to not pollute the main '
1432 '<container> listings.')
1433 parser.add_option('', '--leave-segments', action='store_true',
1434 dest='leave_segments', default=False, help='Indicates that you want '
1435 'the older segments of manifest objects left alone (in the case of '
1436 'overwrites)')
1373 (options, args) = parse_args(parser, args)1437 (options, args) = parse_args(parser, args)
1374 args = args[1:]1438 args = args[1:]
1375 if len(args) < 2:1439 if len(args) < 2:
1376 error_queue.put('Usage: %s [options] %s' %1440 error_queue.put('Usage: %s [options] %s' %
1377 (basename(argv[0]), st_upload_help))1441 (basename(argv[0]), st_upload_help))
1378 return1442 return
13791443 object_queue = Queue(10000)
1380 file_queue = Queue(10000)1444
13811445 def _segment_job(job, conn):
1382 def _upload_file((path, dir_marker), conn):1446 if job.get('delete', False):
1447 conn.delete_object(job['container'], job['obj'])
1448 else:
1449 fp = open(job['path'], 'rb')
1450 fp.seek(job['segment_start'])
1451 conn.put_object(job.get('container', args[0] + '_segments'),
1452 job['obj'], fp, content_length=job['segment_size'])
1453 if options.verbose and 'log_line' in job:
1454 print_queue.put(job['log_line'])
1455
1456 def _object_job(job, conn):
1457 path = job['path']
1458 container = job.get('container', args[0])
1459 dir_marker = job.get('dir_marker', False)
1383 try:1460 try:
1384 obj = path1461 obj = path
1385 if obj.startswith('./') or obj.startswith('.\\'):1462 if obj.startswith('./') or obj.startswith('.\\'):
@@ -1388,7 +1465,7 @@
1388 if dir_marker:1465 if dir_marker:
1389 if options.changed:1466 if options.changed:
1390 try:1467 try:
1391 headers = conn.head_object(args[0], obj)1468 headers = conn.head_object(container, obj)
1392 ct = headers.get('content-type')1469 ct = headers.get('content-type')
1393 cl = int(headers.get('content-length'))1470 cl = int(headers.get('content-length'))
1394 et = headers.get('etag')1471 et = headers.get('etag')
@@ -1401,24 +1478,86 @@
1401 except ClientException, err:1478 except ClientException, err:
1402 if err.http_status != 404:1479 if err.http_status != 404:
1403 raise1480 raise
1404 conn.put_object(args[0], obj, '', content_length=0,1481 conn.put_object(container, obj, '', content_length=0,
1405 content_type='text/directory',1482 content_type='text/directory',
1406 headers=put_headers)1483 headers=put_headers)
1407 else:1484 else:
1408 if options.changed:1485 # We need to HEAD all objects now in case we're overwriting a
1486 # manifest object and need to delete the old segments
1487 # ourselves.
1488 old_manifest = None
1489 if options.changed or not options.leave_segments:
1409 try:1490 try:
1410 headers = conn.head_object(args[0], obj)1491 headers = conn.head_object(container, obj)
1411 cl = int(headers.get('content-length'))1492 cl = int(headers.get('content-length'))
1412 mt = headers.get('x-object-meta-mtime')1493 mt = headers.get('x-object-meta-mtime')
1413 if cl == getsize(path) and \1494 if options.changed and cl == getsize(path) and \
1414 mt == put_headers['x-object-meta-mtime']:1495 mt == put_headers['x-object-meta-mtime']:
1415 return1496 return
1497 if not options.leave_segments:
1498 old_manifest = headers.get('x-object-manifest')
1416 except ClientException, err:1499 except ClientException, err:
1417 if err.http_status != 404:1500 if err.http_status != 404:
1418 raise1501 raise
1419 conn.put_object(args[0], obj, open(path, 'rb'),1502 if options.segment_size and \
1420 content_length=getsize(path),1503 getsize(path) < options.segment_size:
1421 headers=put_headers)1504 full_size = getsize(path)
1505 segment_queue = Queue(10000)
1506 segment_threads = [QueueFunctionThread(segment_queue,
1507 _segment_job, create_connection()) for _ in xrange(10)]
1508 for thread in segment_threads:
1509 thread.start()
1510 segment = 0
1511 segment_start = 0
1512 while segment_start < full_size:
1513 segment_size = int(options.segment_size)
1514 if segment_start + segment_size > full_size:
1515 segment_size = full_size - segment_start
1516 segment_queue.put({'path': path,
1517 'obj': '%s/%s/%s/%08d' % (obj,
1518 put_headers['x-object-meta-mtime'], full_size,
1519 segment),
1520 'segment_start': segment_start,
1521 'segment_size': segment_size,
1522 'log_line': '%s segment %s' % (obj, segment)})
1523 segment += 1
1524 segment_start += segment_size
1525 while not segment_queue.empty():
1526 sleep(0.01)
1527 for thread in segment_threads:
1528 thread.abort = True
1529 while thread.isAlive():
1530 thread.join(0.01)
1531 new_object_manifest = '%s_segments/%s/%s/%s/' % (
1532 container, obj, put_headers['x-object-meta-mtime'],
1533 full_size)
1534 if old_manifest == new_object_manifest:
1535 old_manifest = None
1536 put_headers['x-object-manifest'] = new_object_manifest
1537 conn.put_object(container, obj, '', content_length=0,
1538 headers=put_headers)
1539 else:
1540 conn.put_object(container, obj, open(path, 'rb'),
1541 content_length=getsize(path), headers=put_headers)
1542 if old_manifest:
1543 segment_queue = Queue(10000)
1544 scontainer, sprefix = old_manifest.split('/', 1)
1545 for delobj in conn.get_container(scontainer,
1546 prefix=sprefix)[1]:
1547 segment_queue.put({'delete': True,
1548 'container': scontainer, 'obj': delobj['name']})
1549 if not segment_queue.empty():
1550 segment_threads = [QueueFunctionThread(segment_queue,
1551 _segment_job, create_connection()) for _ in
1552 xrange(10)]
1553 for thread in segment_threads:
1554 thread.start()
1555 while not segment_queue.empty():
1556 sleep(0.01)
1557 for thread in segment_threads:
1558 thread.abort = True
1559 while thread.isAlive():
1560 thread.join(0.01)
1422 if options.verbose:1561 if options.verbose:
1423 print_queue.put(obj)1562 print_queue.put(obj)
1424 except OSError, err:1563 except OSError, err:
@@ -1429,22 +1568,22 @@
1429 def _upload_dir(path):1568 def _upload_dir(path):
1430 names = listdir(path)1569 names = listdir(path)
1431 if not names:1570 if not names:
1432 file_queue.put((path, True)) # dir_marker = True1571 object_queue.put({'path': path, 'dir_marker': True})
1433 else:1572 else:
1434 for name in listdir(path):1573 for name in listdir(path):
1435 subpath = join(path, name)1574 subpath = join(path, name)
1436 if isdir(subpath):1575 if isdir(subpath):
1437 _upload_dir(subpath)1576 _upload_dir(subpath)
1438 else:1577 else:
1439 file_queue.put((subpath, False)) # dir_marker = False1578 object_queue.put({'path': subpath})
14401579
1441 url, token = get_auth(options.auth, options.user, options.key,1580 url, token = get_auth(options.auth, options.user, options.key,
1442 snet=options.snet)1581 snet=options.snet)
1443 create_connection = lambda: Connection(options.auth, options.user,1582 create_connection = lambda: Connection(options.auth, options.user,
1444 options.key, preauthurl=url, preauthtoken=token, snet=options.snet)1583 options.key, preauthurl=url, preauthtoken=token, snet=options.snet)
1445 file_threads = [QueueFunctionThread(file_queue, _upload_file,1584 object_threads = [QueueFunctionThread(object_queue, _object_job,
1446 create_connection()) for _ in xrange(10)]1585 create_connection()) for _ in xrange(10)]
1447 for thread in file_threads:1586 for thread in object_threads:
1448 thread.start()1587 thread.start()
1449 conn = create_connection()1588 conn = create_connection()
1450 # Try to create the container, just in case it doesn't exist. If this1589 # Try to create the container, just in case it doesn't exist. If this
@@ -1453,6 +1592,8 @@
1453 # it'll surface on the first object PUT.1592 # it'll surface on the first object PUT.
1454 try:1593 try:
1455 conn.put_container(args[0])1594 conn.put_container(args[0])
1595 if options.segment_size is not None:
1596 conn.put_container(args[0] + '_segments')
1456 except:1597 except:
1457 pass1598 pass
1458 try:1599 try:
@@ -1460,10 +1601,10 @@
1460 if isdir(arg):1601 if isdir(arg):
1461 _upload_dir(arg)1602 _upload_dir(arg)
1462 else:1603 else:
1463 file_queue.put((arg, False)) # dir_marker = False1604 object_queue.put({'path': arg})
1464 while not file_queue.empty():1605 while not object_queue.empty():
1465 sleep(0.01)1606 sleep(0.01)
1466 for thread in file_threads:1607 for thread in object_threads:
1467 thread.abort = True1608 thread.abort = True
1468 while thread.isAlive():1609 while thread.isAlive():
1469 thread.join(0.01)1610 thread.join(0.01)
14701611
=== modified file 'doc/source/index.rst'
--- doc/source/index.rst 2010-11-12 18:54:07 +0000
+++ doc/source/index.rst 2010-12-13 22:17:44 +0000
@@ -44,6 +44,7 @@
44 overview_replication44 overview_replication
45 overview_stats45 overview_stats
46 ratelimit46 ratelimit
47 overview_large_objects
4748
48Developer Documentation49Developer Documentation
49=======================50=======================
5051
=== added file 'doc/source/overview_large_objects.rst'
--- doc/source/overview_large_objects.rst 1970-01-01 00:00:00 +0000
+++ doc/source/overview_large_objects.rst 2010-12-13 22:17:44 +0000
@@ -0,0 +1,177 @@
1====================
2Large Object Support
3====================
4
5--------
6Overview
7--------
8
9Swift has a limit on the size of a single uploaded object; by default this is
105GB. However, the download size of a single object is virtually unlimited with
11the concept of segmentation. Segments of the larger object are uploaded and a
12special manifest file is created that, when downloaded, sends all the segments
13concatenated as a single object. This also offers much greater upload speed
14with the possibility of parallel uploads of the segments.
15
16----------------------------------
17Using ``st`` for Segmented Objects
18----------------------------------
19
20The quickest way to try out this feature is use the included ``st`` Swift Tool.
21You can use the ``-S`` option to specify the segment size to use when splitting
22a large file. For example::
23
24 st upload test_container -S 1073741824 large_file
25
26This would split the large_file into 1G segments and begin uploading those
27segments in parallel. Once all the segments have been uploaded, ``st`` will
28then create the manifest file so the segments can be downloaded as one.
29
30So now, the following ``st`` command would download the entire large object::
31
32 st download test_container large_file
33
34``st`` uses a strict convention for its segmented object support. In the above
35example it will upload all the segments into a second container named
36test_container_segments. These segments will have names like
37large_file/1290206778.25/21474836480/00000000,
38large_file/1290206778.25/21474836480/00000001, etc.
39
40The main benefit for using a separate container is that the main container
41listings will not be polluted with all the segment names. The reason for using
42the segment name format of <name>/<timestamp>/<size>/<segment> is so that an
43upload of a new file with the same name won't overwrite the contents of the
44first until the last moment when the manifest file is updated.
45
46``st`` will manage these segment files for you, deleting old segments on
47deletes and overwrites, etc. You can override this behavior with the
48``--leave-segments`` option if desired; this is useful if you want to have
49multiple versions of the same large object available.
50
51----------
52Direct API
53----------
54
55You can also work with the segments and manifests directly with HTTP requests
56instead of having ``st`` do that for you. You can just upload the segments like
57you would any other object and the manifest is just a zero-byte file with an
58extra ``X-Object-Manifest`` header.
59
60All the object segments need to be in the same container, have a common object
61name prefix, and their names sort in the order they should be concatenated.
62They don't have to be in the same container as the manifest file will be, which
63is useful to keep container listings clean as explained above with ``st``.
64
65The manifest file is simply a zero-byte file with the extra
66``X-Object-Manifest: <container>/<prefix>`` header, where ``<container>`` is
67the container the object segments are in and ``<prefix>`` is the common prefix
68for all the segments.
69
70It is best to upload all the segments first and then create or update the
71manifest. In this way, the full object won't be available for downloading until
72the upload is complete. Also, you can upload a new set of segments to a second
73location and then update the manifest to point to this new location. During the
74upload of the new segments, the original manifest will still be available to
75download the first set of segments.
76
77Here's an example using ``curl`` with tiny 1-byte segments::
78
79 # First, upload the segments
80 curl -X PUT -H 'X-Auth-Token: <token>' \
81 http://<storage_url>/container/myobject/1 --data-binary '1'
82 curl -X PUT -H 'X-Auth-Token: <token>' \
83 http://<storage_url>/container/myobject/2 --data-binary '2'
84 curl -X PUT -H 'X-Auth-Token: <token>' \
85 http://<storage_url>/container/myobject/3 --data-binary '3'
86
87 # Next, create the manifest file
88 curl -X PUT -H 'X-Auth-Token: <token>' \
89 -H 'X-Object-Manifest: container/myobject/' \
90 http://<storage_url>/container/myobject --data-binary ''
91
92 # And now we can download the segments as a single object
93 curl -H 'X-Auth-Token: <token>' \
94 http://<storage_url>/container/myobject
95
96----------------
97Additional Notes
98----------------
99
100* With a ``GET`` or ``HEAD`` of a manifest file, the ``X-Object-Manifest:
101 <container>/<prefix>`` header will be returned with the concatenated object
102 so you can tell where it's getting its segments from.
103
104* The response's ``Content-Length`` for a ``GET`` or ``HEAD`` on the manifest
105 file will be the sum of all the segments in the ``<container>/<prefix>``
106 listing, dynamically. So, uploading additional segments after the manifest is
107 created will cause the concatenated object to be that much larger; there's no
108 need to recreate the manifest file.
109
110* The response's ``Content-Type`` for a ``GET`` or ``HEAD`` on the manifest
111 will be the same as the ``Content-Type`` set during the ``PUT`` request that
112 created the manifest. You can easily change the ``Content-Type`` by reissuing
113 the ``PUT``.
114
115* The response's ``ETag`` for a ``GET`` or ``HEAD`` on the manifest file will
116 be the MD5 sum of the concatenated string of ETags for each of the segments
117 in the ``<container>/<prefix>`` listing, dynamically. Usually in Swift the
118 ETag is the MD5 sum of the contents of the object, and that holds true for
119 each segment independently. But, it's not feasible to generate such an ETag
120 for the manifest itself, so this method was chosen to at least offer change
121 detection.
122
123-------
124History
125-------
126
127Large object support has gone through various iterations before settling on
128this implementation.
129
130The primary factor driving the limitation of object size in swift is
131maintaining balance among the partitions of the ring. To maintain an even
132dispersion of disk usage throughout the cluster the obvious storage pattern
133was to simply split larger objects into smaller segments, which could then be
134glued together during a read.
135
136Before the introduction of large object support some applications were already
137splitting their uploads into segments and re-assembling them on the client
138side after retrieving the individual pieces. This design allowed the client
139to support backup and archiving of large data sets, but was also frequently
140employed to improve performance or reduce errors due to network interruption.
141The major disadvantage of this method is that knowledge of the original
142partitioning scheme is required to properly reassemble the object, which is
143not practical for some use cases, such as CDN origination.
144
145In order to eliminate any barrier to entry for clients wanting to store
146objects larger than 5GB, initially we also prototyped fully transparent
147support for large object uploads. A fully transparent implementation would
148support a larger max size by automatically splitting objects into segments
149during upload within the proxy without any changes to the client API. All
150segments were completely hidden from the client API.
151
152This solution introduced a number of challenging failure conditions into the
153cluster, wouldn't provide the client with any option to do parallel uploads,
154and had no basis for a resume feature. The transparent implementation was
155deemed just too complex for the benefit.
156
157The current "user manifest" design was chosen in order to provide a
158transparent download of large objects to the client and still provide the
159uploading client a clean API to support segmented uploads.
160
161Alternative "explicit" user manifest options were discussed which would have
162required a pre-defined format for listing the segments to "finalize" the
163segmented upload. While this may offer some potential advantages, it was
164decided that pushing an added burden onto the client which could potentially
165limit adoption should be avoided in favor of a simpler "API" (essentially just
166the format of the 'X-Object-Manifest' header).
167
168During development it was noted that this "implicit" user manifest approach
169which is based on the path prefix can be potentially affected by the eventual
170consistency window of the container listings, which could theoretically cause
171a GET on the manifest object to return an invalid whole object for that short
172term. In reality you're unlikely to encounter this scenario unless you're
173running very high concurrency uploads against a small testing environment
174which isn't running the object-updaters or container-replicators.
175
176Like all of swift, Large Object Support is living feature which will continue
177to improve and may change over time.
0178
=== modified file 'swift/common/client.py'
--- swift/common/client.py 2010-11-18 21:40:40 +0000
+++ swift/common/client.py 2010-12-13 22:17:44 +0000
@@ -569,7 +569,8 @@
569 :param container: container name that the object is in569 :param container: container name that the object is in
570 :param name: object name to put570 :param name: object name to put
571 :param contents: a string or a file like object to read object data from571 :param contents: a string or a file like object to read object data from
572 :param content_length: value to send as content-length header572 :param content_length: value to send as content-length header; also limits
573 the amount read from contents
573 :param etag: etag of contents574 :param etag: etag of contents
574 :param chunk_size: chunk size of data to write575 :param chunk_size: chunk size of data to write
575 :param content_type: value to send as content-type header576 :param content_type: value to send as content-type header
@@ -599,18 +600,24 @@
599 conn.putrequest('PUT', path)600 conn.putrequest('PUT', path)
600 for header, value in headers.iteritems():601 for header, value in headers.iteritems():
601 conn.putheader(header, value)602 conn.putheader(header, value)
602 if not content_length:603 if content_length is None:
603 conn.putheader('Transfer-Encoding', 'chunked')604 conn.putheader('Transfer-Encoding', 'chunked')
604 conn.endheaders()605 conn.endheaders()
605 chunk = contents.read(chunk_size)606 chunk = contents.read(chunk_size)
606 while chunk:607 while chunk:
607 if not content_length:
608 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))608 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
609 else:609 chunk = contents.read(chunk_size)
610 conn.send('0\r\n\r\n')
611 else:
612 conn.endheaders()
613 left = content_length
614 while left > 0:
615 size = chunk_size
616 if size > left:
617 size = left
618 chunk = contents.read(size)
610 conn.send(chunk)619 conn.send(chunk)
611 chunk = contents.read(chunk_size)620 left -= len(chunk)
612 if not content_length:
613 conn.send('0\r\n\r\n')
614 else:621 else:
615 conn.request('PUT', path, contents, headers)622 conn.request('PUT', path, contents, headers)
616 resp = conn.getresponse()623 resp = conn.getresponse()
617624
=== modified file 'swift/common/constraints.py'
--- swift/common/constraints.py 2010-10-26 15:13:14 +0000
+++ swift/common/constraints.py 2010-12-13 22:17:44 +0000
@@ -113,6 +113,17 @@
113 if not check_utf8(req.headers['Content-Type']):113 if not check_utf8(req.headers['Content-Type']):
114 return HTTPBadRequest(request=req, body='Invalid Content-Type',114 return HTTPBadRequest(request=req, body='Invalid Content-Type',
115 content_type='text/plain')115 content_type='text/plain')
116 if 'x-object-manifest' in req.headers:
117 value = req.headers['x-object-manifest']
118 container = prefix = None
119 try:
120 container, prefix = value.split('/', 1)
121 except ValueError:
122 pass
123 if not container or not prefix or '?' in value or '&' in value or \
124 prefix[0] == '/':
125 return HTTPBadRequest(request=req,
126 body='X-Object-Manifest must in the format container/prefix')
116 return check_metadata(req, 'object')127 return check_metadata(req, 'object')
117128
118129
119130
=== modified file 'swift/obj/server.py'
--- swift/obj/server.py 2010-11-01 21:47:48 +0000
+++ swift/obj/server.py 2010-12-13 22:17:44 +0000
@@ -391,6 +391,9 @@
391 'ETag': etag,391 'ETag': etag,
392 'Content-Length': str(os.fstat(fd).st_size),392 'Content-Length': str(os.fstat(fd).st_size),
393 }393 }
394 if 'x-object-manifest' in request.headers:
395 metadata['X-Object-Manifest'] = \
396 request.headers['x-object-manifest']
394 metadata.update(val for val in request.headers.iteritems()397 metadata.update(val for val in request.headers.iteritems()
395 if val[0].lower().startswith('x-object-meta-') and398 if val[0].lower().startswith('x-object-meta-') and
396 len(val[0]) > 14)399 len(val[0]) > 14)
@@ -460,7 +463,8 @@
460 'application/octet-stream'), app_iter=file,463 'application/octet-stream'), app_iter=file,
461 request=request, conditional_response=True)464 request=request, conditional_response=True)
462 for key, value in file.metadata.iteritems():465 for key, value in file.metadata.iteritems():
463 if key.lower().startswith('x-object-meta-'):466 if key == 'X-Object-Manifest' or \
467 key.lower().startswith('x-object-meta-'):
464 response.headers[key] = value468 response.headers[key] = value
465 response.etag = file.metadata['ETag']469 response.etag = file.metadata['ETag']
466 response.last_modified = float(file.metadata['X-Timestamp'])470 response.last_modified = float(file.metadata['X-Timestamp'])
@@ -488,7 +492,8 @@
488 response = Response(content_type=file.metadata['Content-Type'],492 response = Response(content_type=file.metadata['Content-Type'],
489 request=request, conditional_response=True)493 request=request, conditional_response=True)
490 for key, value in file.metadata.iteritems():494 for key, value in file.metadata.iteritems():
491 if key.lower().startswith('x-object-meta-'):495 if key == 'X-Object-Manifest' or \
496 key.lower().startswith('x-object-meta-'):
492 response.headers[key] = value497 response.headers[key] = value
493 response.etag = file.metadata['ETag']498 response.etag = file.metadata['ETag']
494 response.last_modified = float(file.metadata['X-Timestamp'])499 response.last_modified = float(file.metadata['X-Timestamp'])
495500
=== modified file 'swift/proxy/server.py'
--- swift/proxy/server.py 2010-12-13 18:25:09 +0000
+++ swift/proxy/server.py 2010-12-13 22:17:44 +0000
@@ -14,6 +14,10 @@
14# limitations under the License.14# limitations under the License.
1515
16from __future__ import with_statement16from __future__ import with_statement
17try:
18 import simplejson as json
19except ImportError:
20 import json
17import mimetypes21import mimetypes
18import os22import os
19import time23import time
@@ -22,6 +26,7 @@
22from urllib import unquote, quote26from urllib import unquote, quote
23import uuid27import uuid
24import functools28import functools
29from hashlib import md5
2530
26from eventlet.timeout import Timeout31from eventlet.timeout import Timeout
27from webob.exc import HTTPBadRequest, HTTPMethodNotAllowed, \32from webob.exc import HTTPBadRequest, HTTPMethodNotAllowed, \
@@ -36,8 +41,8 @@
36 cache_from_env41 cache_from_env
37from swift.common.bufferedhttp import http_connect42from swift.common.bufferedhttp import http_connect
38from swift.common.constraints import check_metadata, check_object_creation, \43from swift.common.constraints import check_metadata, check_object_creation, \
39 check_utf8, MAX_ACCOUNT_NAME_LENGTH, MAX_CONTAINER_NAME_LENGTH, \44 check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \
40 MAX_FILE_SIZE45 MAX_CONTAINER_NAME_LENGTH, MAX_FILE_SIZE
41from swift.common.exceptions import ChunkReadTimeout, \46from swift.common.exceptions import ChunkReadTimeout, \
42 ChunkWriteTimeout, ConnectionTimeout47 ChunkWriteTimeout, ConnectionTimeout
4348
@@ -95,6 +100,154 @@
95 return 'container/%s/%s' % (account, container)100 return 'container/%s/%s' % (account, container)
96101
97102
103class SegmentedIterable(object):
104 """
105 Iterable that returns the object contents for a segmented object in Swift.
106
107 If set, the response's `bytes_transferred` value will be updated (used to
108 log the size of the request). Also, if there's a failure that cuts the
109 transfer short, the response's `status_int` will be updated (again, just
110 for logging since the original status would have already been sent to the
111 client).
112
113 :param controller: The ObjectController instance to work with.
114 :param container: The container the object segments are within.
115 :param listing: The listing of object segments to iterate over; this may
116 be an iterator or list that returns dicts with 'name' and
117 'bytes' keys.
118 :param response: The webob.Response this iterable is associated with, if
119 any (default: None)
120 """
121
122 def __init__(self, controller, container, listing, response=None):
123 self.controller = controller
124 self.container = container
125 self.listing = iter(listing)
126 self.segment = -1
127 self.segment_dict = None
128 self.segment_peek = None
129 self.seek = 0
130 self.segment_iter = None
131 self.position = 0
132 self.response = response
133 if not self.response:
134 self.response = Response()
135
136 def _load_next_segment(self):
137 """
138 Loads the self.segment_iter with the next object segment's contents.
139
140 :raises: StopIteration when there are no more object segments.
141 """
142 try:
143 self.segment += 1
144 self.segment_dict = self.segment_peek or self.listing.next()
145 self.segment_peek = None
146 partition, nodes = self.controller.app.object_ring.get_nodes(
147 self.controller.account_name, self.container,
148 self.segment_dict['name'])
149 path = '/%s/%s/%s' % (self.controller.account_name, self.container,
150 self.segment_dict['name'])
151 req = Request.blank(path)
152 if self.seek:
153 req.range = 'bytes=%s-' % self.seek
154 self.seek = 0
155 resp = self.controller.GETorHEAD_base(req, 'Object', partition,
156 self.controller.iter_nodes(partition, nodes,
157 self.controller.app.object_ring), path,
158 self.controller.app.object_ring.replica_count)
159 if resp.status_int // 100 != 2:
160 raise Exception('Could not load object segment %s: %s' % (path,
161 resp.status_int))
162 self.segment_iter = resp.app_iter
163 except StopIteration:
164 raise
165 except Exception, err:
166 if not getattr(err, 'swift_logged', False):
167 self.controller.app.logger.exception('ERROR: While processing '
168 'manifest /%s/%s/%s %s' % (self.controller.account_name,
169 self.controller.container_name,
170 self.controller.object_name, self.controller.trans_id))
171 err.swift_logged = True
172 self.response.status_int = 503
173 raise
174
175 def __iter__(self):
176 """ Standard iterator function that returns the object's contents. """
177 try:
178 while True:
179 if not self.segment_iter:
180 self._load_next_segment()
181 while True:
182 with ChunkReadTimeout(self.controller.app.node_timeout):
183 try:
184 chunk = self.segment_iter.next()
185 break
186 except StopIteration:
187 self._load_next_segment()
188 self.position += len(chunk)
189 self.response.bytes_transferred = getattr(self.response,
190 'bytes_transferred', 0) + len(chunk)
191 yield chunk
192 except StopIteration:
193 raise
194 except Exception, err:
195 if not getattr(err, 'swift_logged', False):
196 self.controller.app.logger.exception('ERROR: While processing '
197 'manifest /%s/%s/%s %s' % (self.controller.account_name,
198 self.controller.container_name,
199 self.controller.object_name, self.controller.trans_id))
200 err.swift_logged = True
201 self.response.status_int = 503
202 raise
203
204 def app_iter_range(self, start, stop):
205 """
206 Non-standard iterator function for use with Webob in serving Range
207 requests more quickly. This will skip over segments and do a range
208 request on the first segment to return data from, if needed.
209
210 :param start: The first byte (zero-based) to return. None for 0.
211 :param stop: The last byte (zero-based) to return. None for end.
212 """
213 try:
214 if start:
215 self.segment_peek = self.listing.next()
216 while start >= self.position + self.segment_peek['bytes']:
217 self.segment += 1
218 self.position += self.segment_peek['bytes']
219 self.segment_peek = self.listing.next()
220 self.seek = start - self.position
221 else:
222 start = 0
223 if stop is not None:
224 length = stop - start
225 else:
226 length = None
227 for chunk in self:
228 if length is not None:
229 length -= len(chunk)
230 if length < 0:
231 # Chop off the extra:
232 self.response.bytes_transferred = \
233 getattr(self.response, 'bytes_transferred', 0) \
234 + length
235 yield chunk[:length]
236 break
237 yield chunk
238 except StopIteration:
239 raise
240 except Exception, err:
241 if not getattr(err, 'swift_logged', False):
242 self.controller.app.logger.exception('ERROR: While processing '
243 'manifest /%s/%s/%s %s' % (self.controller.account_name,
244 self.controller.container_name,
245 self.controller.object_name, self.controller.trans_id))
246 err.swift_logged = True
247 self.response.status_int = 503
248 raise
249
250
98class Controller(object):251class Controller(object):
99 """Base WSGI controller class for the proxy"""252 """Base WSGI controller class for the proxy"""
100253
@@ -538,9 +691,118 @@
538 return aresp691 return aresp
539 partition, nodes = self.app.object_ring.get_nodes(692 partition, nodes = self.app.object_ring.get_nodes(
540 self.account_name, self.container_name, self.object_name)693 self.account_name, self.container_name, self.object_name)
541 return self.GETorHEAD_base(req, 'Object', partition,694 resp = self.GETorHEAD_base(req, 'Object', partition,
542 self.iter_nodes(partition, nodes, self.app.object_ring),695 self.iter_nodes(partition, nodes, self.app.object_ring),
543 req.path_info, self.app.object_ring.replica_count)696 req.path_info, self.app.object_ring.replica_count)
697 # If we get a 416 Requested Range Not Satisfiable we have to check if
698 # we were actually requesting a manifest object and then redo the range
699 # request on the whole object.
700 if resp.status_int == 416:
701 req_range = req.range
702 req.range = None
703 resp2 = self.GETorHEAD_base(req, 'Object', partition,
704 self.iter_nodes(partition, nodes, self.app.object_ring),
705 req.path_info, self.app.object_ring.replica_count)
706 if 'x-object-manifest' not in resp2.headers:
707 return resp
708 resp = resp2
709 req.range = req_range
710
711 if 'x-object-manifest' in resp.headers:
712 lcontainer, lprefix = \
713 resp.headers['x-object-manifest'].split('/', 1)
714 lpartition, lnodes = self.app.container_ring.get_nodes(
715 self.account_name, lcontainer)
716 marker = ''
717 listing = []
718 while True:
719 lreq = Request.blank('/%s/%s?prefix=%s&format=json&marker=%s' %
720 (quote(self.account_name), quote(lcontainer),
721 quote(lprefix), quote(marker)))
722 lresp = self.GETorHEAD_base(lreq, 'Container', lpartition,
723 lnodes, lreq.path_info,
724 self.app.container_ring.replica_count)
725 if lresp.status_int // 100 != 2:
726 lresp = HTTPNotFound(request=req)
727 lresp.headers['X-Object-Manifest'] = \
728 resp.headers['x-object-manifest']
729 return lresp
730 if 'swift.authorize' in req.environ:
731 req.acl = lresp.headers.get('x-container-read')
732 aresp = req.environ['swift.authorize'](req)
733 if aresp:
734 return aresp
735 sublisting = json.loads(lresp.body)
736 if not sublisting:
737 break
738 listing.extend(sublisting)
739 if len(listing) > CONTAINER_LISTING_LIMIT:
740 break
741 marker = sublisting[-1]['name']
742
743 if len(listing) > CONTAINER_LISTING_LIMIT:
744 # We will serve large objects with a ton of segments with
745 # chunked transfer encoding.
746
747 def listing_iter():
748 marker = ''
749 while True:
750 lreq = Request.blank(
751 '/%s/%s?prefix=%s&format=json&marker=%s' %
752 (quote(self.account_name), quote(lcontainer),
753 quote(lprefix), quote(marker)))
754 lresp = self.GETorHEAD_base(lreq, 'Container',
755 lpartition, lnodes, lreq.path_info,
756 self.app.container_ring.replica_count)
757 if lresp.status_int // 100 != 2:
758 raise Exception('Object manifest GET could not '
759 'continue listing: %s %s' %
760 (req.path, lreq.path))
761 if 'swift.authorize' in req.environ:
762 req.acl = lresp.headers.get('x-container-read')
763 aresp = req.environ['swift.authorize'](req)
764 if aresp:
765 raise Exception('Object manifest GET could '
766 'not continue listing: %s %s' %
767 (req.path, aresp))
768 sublisting = json.loads(lresp.body)
769 if not sublisting:
770 break
771 for obj in sublisting:
772 yield obj
773 marker = sublisting[-1]['name']
774
775 headers = {
776 'X-Object-Manifest': resp.headers['x-object-manifest'],
777 'Content-Type': resp.content_type}
778 for key, value in resp.headers.iteritems():
779 if key.lower().startswith('x-object-meta-'):
780 headers[key] = value
781 resp = Response(headers=headers, request=req,
782 conditional_response=True)
783 resp.app_iter = SegmentedIterable(self, lcontainer,
784 listing_iter(), resp)
785
786 else:
787 # For objects with a reasonable number of segments, we'll serve
788 # them with a set content-length and computed etag.
789 content_length = sum(o['bytes'] for o in listing)
790 etag = md5('"'.join(o['hash'] for o in listing)).hexdigest()
791 headers = {
792 'X-Object-Manifest': resp.headers['x-object-manifest'],
793 'Content-Type': resp.content_type,
794 'Content-Length': content_length,
795 'ETag': etag}
796 for key, value in resp.headers.iteritems():
797 if key.lower().startswith('x-object-meta-'):
798 headers[key] = value
799 resp = Response(headers=headers, request=req,
800 conditional_response=True)
801 resp.app_iter = SegmentedIterable(self, lcontainer, listing,
802 resp)
803 resp.content_length = content_length
804
805 return resp
544806
545 @public807 @public
546 @delay_denial808 @delay_denial
547809
=== modified file 'test/functionalnosetests/test_object.py'
--- test/functionalnosetests/test_object.py 2010-11-04 19:39:29 +0000
+++ test/functionalnosetests/test_object.py 2010-12-13 22:17:44 +0000
@@ -16,6 +16,7 @@
16 if skip:16 if skip:
17 raise SkipTest17 raise SkipTest
18 self.container = uuid4().hex18 self.container = uuid4().hex
19
19 def put(url, token, parsed, conn):20 def put(url, token, parsed, conn):
20 conn.request('PUT', parsed.path + '/' + self.container, '',21 conn.request('PUT', parsed.path + '/' + self.container, '',
21 {'X-Auth-Token': token})22 {'X-Auth-Token': token})
@@ -24,6 +25,7 @@
24 resp.read()25 resp.read()
25 self.assertEquals(resp.status, 201)26 self.assertEquals(resp.status, 201)
26 self.obj = uuid4().hex27 self.obj = uuid4().hex
28
27 def put(url, token, parsed, conn):29 def put(url, token, parsed, conn):
28 conn.request('PUT', '%s/%s/%s' % (parsed.path, self.container,30 conn.request('PUT', '%s/%s/%s' % (parsed.path, self.container,
29 self.obj), 'test', {'X-Auth-Token': token})31 self.obj), 'test', {'X-Auth-Token': token})
@@ -35,6 +37,7 @@
35 def tearDown(self):37 def tearDown(self):
36 if skip:38 if skip:
37 raise SkipTest39 raise SkipTest
40
38 def delete(url, token, parsed, conn):41 def delete(url, token, parsed, conn):
39 conn.request('DELETE', '%s/%s/%s' % (parsed.path, self.container,42 conn.request('DELETE', '%s/%s/%s' % (parsed.path, self.container,
40 self.obj), '', {'X-Auth-Token': token})43 self.obj), '', {'X-Auth-Token': token})
@@ -42,6 +45,7 @@
42 resp = retry(delete)45 resp = retry(delete)
43 resp.read()46 resp.read()
44 self.assertEquals(resp.status, 204)47 self.assertEquals(resp.status, 204)
48
45 def delete(url, token, parsed, conn):49 def delete(url, token, parsed, conn):
46 conn.request('DELETE', parsed.path + '/' + self.container, '',50 conn.request('DELETE', parsed.path + '/' + self.container, '',
47 {'X-Auth-Token': token})51 {'X-Auth-Token': token})
@@ -53,6 +57,7 @@
53 def test_public_object(self):57 def test_public_object(self):
54 if skip:58 if skip:
55 raise SkipTest59 raise SkipTest
60
56 def get(url, token, parsed, conn):61 def get(url, token, parsed, conn):
57 conn.request('GET',62 conn.request('GET',
58 '%s/%s/%s' % (parsed.path, self.container, self.obj))63 '%s/%s/%s' % (parsed.path, self.container, self.obj))
@@ -62,6 +67,7 @@
62 raise Exception('Should not have been able to GET')67 raise Exception('Should not have been able to GET')
63 except Exception, err:68 except Exception, err:
64 self.assert_(str(err).startswith('No result after '))69 self.assert_(str(err).startswith('No result after '))
70
65 def post(url, token, parsed, conn):71 def post(url, token, parsed, conn):
66 conn.request('POST', parsed.path + '/' + self.container, '',72 conn.request('POST', parsed.path + '/' + self.container, '',
67 {'X-Auth-Token': token,73 {'X-Auth-Token': token,
@@ -73,6 +79,7 @@
73 resp = retry(get)79 resp = retry(get)
74 resp.read()80 resp.read()
75 self.assertEquals(resp.status, 200)81 self.assertEquals(resp.status, 200)
82
76 def post(url, token, parsed, conn):83 def post(url, token, parsed, conn):
77 conn.request('POST', parsed.path + '/' + self.container, '',84 conn.request('POST', parsed.path + '/' + self.container, '',
78 {'X-Auth-Token': token, 'X-Container-Read': ''})85 {'X-Auth-Token': token, 'X-Container-Read': ''})
@@ -89,6 +96,7 @@
89 def test_private_object(self):96 def test_private_object(self):
90 if skip or skip3:97 if skip or skip3:
91 raise SkipTest98 raise SkipTest
99
92 # Ensure we can't access the object with the third account100 # Ensure we can't access the object with the third account
93 def get(url, token, parsed, conn):101 def get(url, token, parsed, conn):
94 conn.request('GET', '%s/%s/%s' % (parsed.path, self.container,102 conn.request('GET', '%s/%s/%s' % (parsed.path, self.container,
@@ -98,8 +106,10 @@
98 resp = retry(get, use_account=3)106 resp = retry(get, use_account=3)
99 resp.read()107 resp.read()
100 self.assertEquals(resp.status, 403)108 self.assertEquals(resp.status, 403)
109
101 # create a shared container writable by account3110 # create a shared container writable by account3
102 shared_container = uuid4().hex111 shared_container = uuid4().hex
112
103 def put(url, token, parsed, conn):113 def put(url, token, parsed, conn):
104 conn.request('PUT', '%s/%s' % (parsed.path,114 conn.request('PUT', '%s/%s' % (parsed.path,
105 shared_container), '',115 shared_container), '',
@@ -110,6 +120,7 @@
110 resp = retry(put)120 resp = retry(put)
111 resp.read()121 resp.read()
112 self.assertEquals(resp.status, 201)122 self.assertEquals(resp.status, 201)
123
113 # verify third account can not copy from private container124 # verify third account can not copy from private container
114 def copy(url, token, parsed, conn):125 def copy(url, token, parsed, conn):
115 conn.request('PUT', '%s/%s/%s' % (parsed.path,126 conn.request('PUT', '%s/%s/%s' % (parsed.path,
@@ -123,6 +134,7 @@
123 resp = retry(copy, use_account=3)134 resp = retry(copy, use_account=3)
124 resp.read()135 resp.read()
125 self.assertEquals(resp.status, 403)136 self.assertEquals(resp.status, 403)
137
126 # verify third account can write "obj1" to shared container138 # verify third account can write "obj1" to shared container
127 def put(url, token, parsed, conn):139 def put(url, token, parsed, conn):
128 conn.request('PUT', '%s/%s/%s' % (parsed.path, shared_container,140 conn.request('PUT', '%s/%s/%s' % (parsed.path, shared_container,
@@ -131,6 +143,7 @@
131 resp = retry(put, use_account=3)143 resp = retry(put, use_account=3)
132 resp.read()144 resp.read()
133 self.assertEquals(resp.status, 201)145 self.assertEquals(resp.status, 201)
146
134 # verify third account can copy "obj1" to shared container147 # verify third account can copy "obj1" to shared container
135 def copy2(url, token, parsed, conn):148 def copy2(url, token, parsed, conn):
136 conn.request('COPY', '%s/%s/%s' % (parsed.path,149 conn.request('COPY', '%s/%s/%s' % (parsed.path,
@@ -143,6 +156,7 @@
143 resp = retry(copy2, use_account=3)156 resp = retry(copy2, use_account=3)
144 resp.read()157 resp.read()
145 self.assertEquals(resp.status, 201)158 self.assertEquals(resp.status, 201)
159
146 # verify third account STILL can not copy from private container160 # verify third account STILL can not copy from private container
147 def copy3(url, token, parsed, conn):161 def copy3(url, token, parsed, conn):
148 conn.request('COPY', '%s/%s/%s' % (parsed.path,162 conn.request('COPY', '%s/%s/%s' % (parsed.path,
@@ -155,6 +169,7 @@
155 resp = retry(copy3, use_account=3)169 resp = retry(copy3, use_account=3)
156 resp.read()170 resp.read()
157 self.assertEquals(resp.status, 403)171 self.assertEquals(resp.status, 403)
172
158 # clean up "obj1"173 # clean up "obj1"
159 def delete(url, token, parsed, conn):174 def delete(url, token, parsed, conn):
160 conn.request('DELETE', '%s/%s/%s' % (parsed.path, shared_container,175 conn.request('DELETE', '%s/%s/%s' % (parsed.path, shared_container,
@@ -163,6 +178,7 @@
163 resp = retry(delete)178 resp = retry(delete)
164 resp.read()179 resp.read()
165 self.assertEquals(resp.status, 204)180 self.assertEquals(resp.status, 204)
181
166 # clean up shared_container182 # clean up shared_container
167 def delete(url, token, parsed, conn):183 def delete(url, token, parsed, conn):
168 conn.request('DELETE',184 conn.request('DELETE',
@@ -173,6 +189,269 @@
173 resp.read()189 resp.read()
174 self.assertEquals(resp.status, 204)190 self.assertEquals(resp.status, 204)
175191
192 def test_manifest(self):
193 if skip:
194 raise SkipTest
195 # Data for the object segments
196 segments1 = ['one', 'two', 'three', 'four', 'five']
197 segments2 = ['six', 'seven', 'eight']
198 segments3 = ['nine', 'ten', 'eleven']
199
200 # Upload the first set of segments
201 def put(url, token, parsed, conn, objnum):
202 conn.request('PUT', '%s/%s/segments1/%s' % (parsed.path,
203 self.container, str(objnum)), segments1[objnum],
204 {'X-Auth-Token': token})
205 return check_response(conn)
206 for objnum in xrange(len(segments1)):
207 resp = retry(put, objnum)
208 resp.read()
209 self.assertEquals(resp.status, 201)
210
211 # Upload the manifest
212 def put(url, token, parsed, conn):
213 conn.request('PUT', '%s/%s/manifest' % (parsed.path,
214 self.container), '', {'X-Auth-Token': token,
215 'X-Object-Manifest': '%s/segments1/' % self.container,
216 'Content-Type': 'text/jibberish', 'Content-Length': '0'})
217 return check_response(conn)
218 resp = retry(put)
219 resp.read()
220 self.assertEquals(resp.status, 201)
221
222 # Get the manifest (should get all the segments as the body)
223 def get(url, token, parsed, conn):
224 conn.request('GET', '%s/%s/manifest' % (parsed.path,
225 self.container), '', {'X-Auth-Token': token})
226 return check_response(conn)
227 resp = retry(get)
228 self.assertEquals(resp.read(), ''.join(segments1))
229 self.assertEquals(resp.status, 200)
230 self.assertEquals(resp.getheader('content-type'), 'text/jibberish')
231
232 # Get with a range at the start of the second segment
233 def get(url, token, parsed, conn):
234 conn.request('GET', '%s/%s/manifest' % (parsed.path,
235 self.container), '', {'X-Auth-Token': token, 'Range':
236 'bytes=3-'})
237 return check_response(conn)
238 resp = retry(get)
239 self.assertEquals(resp.read(), ''.join(segments1[1:]))
240 self.assertEquals(resp.status, 206)
241
242 # Get with a range in the middle of the second segment
243 def get(url, token, parsed, conn):
244 conn.request('GET', '%s/%s/manifest' % (parsed.path,
245 self.container), '', {'X-Auth-Token': token, 'Range':
246 'bytes=5-'})
247 return check_response(conn)
248 resp = retry(get)
249 self.assertEquals(resp.read(), ''.join(segments1)[5:])
250 self.assertEquals(resp.status, 206)
251
252 # Get with a full start and stop range
253 def get(url, token, parsed, conn):
254 conn.request('GET', '%s/%s/manifest' % (parsed.path,
255 self.container), '', {'X-Auth-Token': token, 'Range':
256 'bytes=5-10'})
257 return check_response(conn)
258 resp = retry(get)
259 self.assertEquals(resp.read(), ''.join(segments1)[5:11])
260 self.assertEquals(resp.status, 206)
261
262 # Upload the second set of segments
263 def put(url, token, parsed, conn, objnum):
264 conn.request('PUT', '%s/%s/segments2/%s' % (parsed.path,
265 self.container, str(objnum)), segments2[objnum],
266 {'X-Auth-Token': token})
267 return check_response(conn)
268 for objnum in xrange(len(segments2)):
269 resp = retry(put, objnum)
270 resp.read()
271 self.assertEquals(resp.status, 201)
272
273 # Get the manifest (should still be the first segments of course)
274 def get(url, token, parsed, conn):
275 conn.request('GET', '%s/%s/manifest' % (parsed.path,
276 self.container), '', {'X-Auth-Token': token})
277 return check_response(conn)
278 resp = retry(get)
279 self.assertEquals(resp.read(), ''.join(segments1))
280 self.assertEquals(resp.status, 200)
281
282 # Update the manifest
283 def put(url, token, parsed, conn):
284 conn.request('PUT', '%s/%s/manifest' % (parsed.path,
285 self.container), '', {'X-Auth-Token': token,
286 'X-Object-Manifest': '%s/segments2/' % self.container,
287 'Content-Length': '0'})
288 return check_response(conn)
289 resp = retry(put)
290 resp.read()
291 self.assertEquals(resp.status, 201)
292
293 # Get the manifest (should be the second set of segments now)
294 def get(url, token, parsed, conn):
295 conn.request('GET', '%s/%s/manifest' % (parsed.path,
296 self.container), '', {'X-Auth-Token': token})
297 return check_response(conn)
298 resp = retry(get)
299 self.assertEquals(resp.read(), ''.join(segments2))
300 self.assertEquals(resp.status, 200)
301
302 if not skip3:
303
304 # Ensure we can't access the manifest with the third account
305 def get(url, token, parsed, conn):
306 conn.request('GET', '%s/%s/manifest' % (parsed.path,
307 self.container), '', {'X-Auth-Token': token})
308 return check_response(conn)
309 resp = retry(get, use_account=3)
310 resp.read()
311 self.assertEquals(resp.status, 403)
312
313 # Grant access to the third account
314 def post(url, token, parsed, conn):
315 conn.request('POST', '%s/%s' % (parsed.path, self.container),
316 '', {'X-Auth-Token': token, 'X-Container-Read':
317 swift_test_user[2]})
318 return check_response(conn)
319 resp = retry(post)
320 resp.read()
321 self.assertEquals(resp.status, 204)
322
323 # The third account should be able to get the manifest now
324 def get(url, token, parsed, conn):
325 conn.request('GET', '%s/%s/manifest' % (parsed.path,
326 self.container), '', {'X-Auth-Token': token})
327 return check_response(conn)
328 resp = retry(get, use_account=3)
329 self.assertEquals(resp.read(), ''.join(segments2))
330 self.assertEquals(resp.status, 200)
331
332 # Create another container for the third set of segments
333 acontainer = uuid4().hex
334
335 def put(url, token, parsed, conn):
336 conn.request('PUT', parsed.path + '/' + acontainer, '',
337 {'X-Auth-Token': token})
338 return check_response(conn)
339 resp = retry(put)
340 resp.read()
341 self.assertEquals(resp.status, 201)
342
343 # Upload the third set of segments in the other container
344 def put(url, token, parsed, conn, objnum):
345 conn.request('PUT', '%s/%s/segments3/%s' % (parsed.path,
346 acontainer, str(objnum)), segments3[objnum],
347 {'X-Auth-Token': token})
348 return check_response(conn)
349 for objnum in xrange(len(segments3)):
350 resp = retry(put, objnum)
351 resp.read()
352 self.assertEquals(resp.status, 201)
353
354 # Update the manifest
355 def put(url, token, parsed, conn):
356 conn.request('PUT', '%s/%s/manifest' % (parsed.path,
357 self.container), '', {'X-Auth-Token': token,
358 'X-Object-Manifest': '%s/segments3/' % acontainer,
359 'Content-Length': '0'})
360 return check_response(conn)
361 resp = retry(put)
362 resp.read()
363 self.assertEquals(resp.status, 201)
364
365 # Get the manifest to ensure it's the third set of segments
366 def get(url, token, parsed, conn):
367 conn.request('GET', '%s/%s/manifest' % (parsed.path,
368 self.container), '', {'X-Auth-Token': token})
369 return check_response(conn)
370 resp = retry(get)
371 self.assertEquals(resp.read(), ''.join(segments3))
372 self.assertEquals(resp.status, 200)
373
374 if not skip3:
375
376 # Ensure we can't access the manifest with the third account
377 # (because the segments are in a protected container even if the
378 # manifest itself is not).
379
380 def get(url, token, parsed, conn):
381 conn.request('GET', '%s/%s/manifest' % (parsed.path,
382 self.container), '', {'X-Auth-Token': token})
383 return check_response(conn)
384 resp = retry(get, use_account=3)
385 resp.read()
386 self.assertEquals(resp.status, 403)
387
388 # Grant access to the third account
389 def post(url, token, parsed, conn):
390 conn.request('POST', '%s/%s' % (parsed.path, acontainer),
391 '', {'X-Auth-Token': token, 'X-Container-Read':
392 swift_test_user[2]})
393 return check_response(conn)
394 resp = retry(post)
395 resp.read()
396 self.assertEquals(resp.status, 204)
397
398 # The third account should be able to get the manifest now
399 def get(url, token, parsed, conn):
400 conn.request('GET', '%s/%s/manifest' % (parsed.path,
401 self.container), '', {'X-Auth-Token': token})
402 return check_response(conn)
403 resp = retry(get, use_account=3)
404 self.assertEquals(resp.read(), ''.join(segments3))
405 self.assertEquals(resp.status, 200)
406
407 # Delete the manifest
408 def delete(url, token, parsed, conn, objnum):
409 conn.request('DELETE', '%s/%s/manifest' % (parsed.path,
410 self.container), '', {'X-Auth-Token': token})
411 return check_response(conn)
412 resp = retry(delete, objnum)
413 resp.read()
414 self.assertEquals(resp.status, 204)
415
416 # Delete the third set of segments
417 def delete(url, token, parsed, conn, objnum):
418 conn.request('DELETE', '%s/%s/segments3/%s' % (parsed.path,
419 acontainer, str(objnum)), '', {'X-Auth-Token': token})
420 return check_response(conn)
421 for objnum in xrange(len(segments3)):
422 resp = retry(delete, objnum)
423 resp.read()
424 self.assertEquals(resp.status, 204)
425
426 # Delete the second set of segments
427 def delete(url, token, parsed, conn, objnum):
428 conn.request('DELETE', '%s/%s/segments2/%s' % (parsed.path,
429 self.container, str(objnum)), '', {'X-Auth-Token': token})
430 return check_response(conn)
431 for objnum in xrange(len(segments2)):
432 resp = retry(delete, objnum)
433 resp.read()
434 self.assertEquals(resp.status, 204)
435
436 # Delete the first set of segments
437 def delete(url, token, parsed, conn, objnum):
438 conn.request('DELETE', '%s/%s/segments1/%s' % (parsed.path,
439 self.container, str(objnum)), '', {'X-Auth-Token': token})
440 return check_response(conn)
441 for objnum in xrange(len(segments1)):
442 resp = retry(delete, objnum)
443 resp.read()
444 self.assertEquals(resp.status, 204)
445
446 # Delete the extra container
447 def delete(url, token, parsed, conn):
448 conn.request('DELETE', '%s/%s' % (parsed.path, acontainer), '',
449 {'X-Auth-Token': token})
450 return check_response(conn)
451 resp = retry(delete)
452 resp.read()
453 self.assertEquals(resp.status, 204)
454
176455
177if __name__ == '__main__':456if __name__ == '__main__':
178 unittest.main()457 unittest.main()
179458
=== modified file 'test/unit/common/test_constraints.py'
--- test/unit/common/test_constraints.py 2010-08-16 22:30:27 +0000
+++ test/unit/common/test_constraints.py 2010-12-13 22:17:44 +0000
@@ -22,6 +22,7 @@
2222
23from swift.common import constraints23from swift.common import constraints
2424
25
25class TestConstraints(unittest.TestCase):26class TestConstraints(unittest.TestCase):
2627
27 def test_check_metadata_empty(self):28 def test_check_metadata_empty(self):
@@ -137,6 +138,32 @@
137 self.assert_(isinstance(resp, HTTPBadRequest))138 self.assert_(isinstance(resp, HTTPBadRequest))
138 self.assert_('Content-Type' in resp.body)139 self.assert_('Content-Type' in resp.body)
139140
141 def test_check_object_manifest_header(self):
142 resp = constraints.check_object_creation(Request.blank('/',
143 headers={'X-Object-Manifest': 'container/prefix', 'Content-Length':
144 '0', 'Content-Type': 'text/plain'}), 'manifest')
145 self.assert_(not resp)
146 resp = constraints.check_object_creation(Request.blank('/',
147 headers={'X-Object-Manifest': 'container', 'Content-Length': '0',
148 'Content-Type': 'text/plain'}), 'manifest')
149 self.assert_(isinstance(resp, HTTPBadRequest))
150 resp = constraints.check_object_creation(Request.blank('/',
151 headers={'X-Object-Manifest': '/container/prefix',
152 'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest')
153 self.assert_(isinstance(resp, HTTPBadRequest))
154 resp = constraints.check_object_creation(Request.blank('/',
155 headers={'X-Object-Manifest': 'container/prefix?query=param',
156 'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest')
157 self.assert_(isinstance(resp, HTTPBadRequest))
158 resp = constraints.check_object_creation(Request.blank('/',
159 headers={'X-Object-Manifest': 'container/prefix&query=param',
160 'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest')
161 self.assert_(isinstance(resp, HTTPBadRequest))
162 resp = constraints.check_object_creation(Request.blank('/',
163 headers={'X-Object-Manifest': 'http://host/container/prefix',
164 'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest')
165 self.assert_(isinstance(resp, HTTPBadRequest))
166
140 def test_check_mount(self):167 def test_check_mount(self):
141 self.assertFalse(constraints.check_mount('', ''))168 self.assertFalse(constraints.check_mount('', ''))
142 constraints.os = MockTrue() # mock os module169 constraints.os = MockTrue() # mock os module
143170
=== modified file 'test/unit/obj/test_server.py'
--- test/unit/obj/test_server.py 2010-10-13 21:26:43 +0000
+++ test/unit/obj/test_server.py 2010-12-13 22:17:44 +0000
@@ -42,7 +42,7 @@
42 self.path_to_test_xfs = os.environ.get('PATH_TO_TEST_XFS')42 self.path_to_test_xfs = os.environ.get('PATH_TO_TEST_XFS')
43 if not self.path_to_test_xfs or \43 if not self.path_to_test_xfs or \
44 not os.path.exists(self.path_to_test_xfs):44 not os.path.exists(self.path_to_test_xfs):
45 print >>sys.stderr, 'WARNING: PATH_TO_TEST_XFS not set or not ' \45 print >> sys.stderr, 'WARNING: PATH_TO_TEST_XFS not set or not ' \
46 'pointing to a valid directory.\n' \46 'pointing to a valid directory.\n' \
47 'Please set PATH_TO_TEST_XFS to a directory on an XFS file ' \47 'Please set PATH_TO_TEST_XFS to a directory on an XFS file ' \
48 'system for testing.'48 'system for testing.'
@@ -77,7 +77,8 @@
77 self.assertEquals(resp.status_int, 201)77 self.assertEquals(resp.status_int, 201)
7878
79 timestamp = normalize_timestamp(time())79 timestamp = normalize_timestamp(time())
80 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},80 req = Request.blank('/sda1/p/a/c/o',
81 environ={'REQUEST_METHOD': 'POST'},
81 headers={'X-Timestamp': timestamp,82 headers={'X-Timestamp': timestamp,
82 'X-Object-Meta-3': 'Three',83 'X-Object-Meta-3': 'Three',
83 'X-Object-Meta-4': 'Four',84 'X-Object-Meta-4': 'Four',
@@ -95,7 +96,8 @@
95 if not self.path_to_test_xfs:96 if not self.path_to_test_xfs:
96 raise SkipTest97 raise SkipTest
97 timestamp = normalize_timestamp(time())98 timestamp = normalize_timestamp(time())
98 req = Request.blank('/sda1/p/a/c/fail', environ={'REQUEST_METHOD': 'POST'},99 req = Request.blank('/sda1/p/a/c/fail',
100 environ={'REQUEST_METHOD': 'POST'},
99 headers={'X-Timestamp': timestamp,101 headers={'X-Timestamp': timestamp,
100 'X-Object-Meta-1': 'One',102 'X-Object-Meta-1': 'One',
101 'X-Object-Meta-2': 'Two',103 'X-Object-Meta-2': 'Two',
@@ -116,29 +118,37 @@
116 def test_POST_container_connection(self):118 def test_POST_container_connection(self):
117 if not self.path_to_test_xfs:119 if not self.path_to_test_xfs:
118 raise SkipTest120 raise SkipTest
121
119 def mock_http_connect(response, with_exc=False):122 def mock_http_connect(response, with_exc=False):
123
120 class FakeConn(object):124 class FakeConn(object):
125
121 def __init__(self, status, with_exc):126 def __init__(self, status, with_exc):
122 self.status = status127 self.status = status
123 self.reason = 'Fake'128 self.reason = 'Fake'
124 self.host = '1.2.3.4'129 self.host = '1.2.3.4'
125 self.port = '1234'130 self.port = '1234'
126 self.with_exc = with_exc131 self.with_exc = with_exc
132
127 def getresponse(self):133 def getresponse(self):
128 if self.with_exc:134 if self.with_exc:
129 raise Exception('test')135 raise Exception('test')
130 return self136 return self
137
131 def read(self, amt=None):138 def read(self, amt=None):
132 return ''139 return ''
140
133 return lambda *args, **kwargs: FakeConn(response, with_exc)141 return lambda *args, **kwargs: FakeConn(response, with_exc)
142
134 old_http_connect = object_server.http_connect143 old_http_connect = object_server.http_connect
135 try:144 try:
136 timestamp = normalize_timestamp(time())145 timestamp = normalize_timestamp(time())
137 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},146 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD':
138 headers={'X-Timestamp': timestamp, 'Content-Type': 'text/plain',147 'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type':
139 'Content-Length': '0'})148 'text/plain', 'Content-Length': '0'})
140 resp = self.object_controller.PUT(req)149 resp = self.object_controller.PUT(req)
141 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},150 req = Request.blank('/sda1/p/a/c/o',
151 environ={'REQUEST_METHOD': 'POST'},
142 headers={'X-Timestamp': timestamp,152 headers={'X-Timestamp': timestamp,
143 'X-Container-Host': '1.2.3.4:0',153 'X-Container-Host': '1.2.3.4:0',
144 'X-Container-Partition': '3',154 'X-Container-Partition': '3',
@@ -148,7 +158,8 @@
148 object_server.http_connect = mock_http_connect(202)158 object_server.http_connect = mock_http_connect(202)
149 resp = self.object_controller.POST(req)159 resp = self.object_controller.POST(req)
150 self.assertEquals(resp.status_int, 202)160 self.assertEquals(resp.status_int, 202)
151 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},161 req = Request.blank('/sda1/p/a/c/o',
162 environ={'REQUEST_METHOD': 'POST'},
152 headers={'X-Timestamp': timestamp,163 headers={'X-Timestamp': timestamp,
153 'X-Container-Host': '1.2.3.4:0',164 'X-Container-Host': '1.2.3.4:0',
154 'X-Container-Partition': '3',165 'X-Container-Partition': '3',
@@ -158,7 +169,8 @@
158 object_server.http_connect = mock_http_connect(202, with_exc=True)169 object_server.http_connect = mock_http_connect(202, with_exc=True)
159 resp = self.object_controller.POST(req)170 resp = self.object_controller.POST(req)
160 self.assertEquals(resp.status_int, 202)171 self.assertEquals(resp.status_int, 202)
161 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},172 req = Request.blank('/sda1/p/a/c/o',
173 environ={'REQUEST_METHOD': 'POST'},
162 headers={'X-Timestamp': timestamp,174 headers={'X-Timestamp': timestamp,
163 'X-Container-Host': '1.2.3.4:0',175 'X-Container-Host': '1.2.3.4:0',
164 'X-Container-Partition': '3',176 'X-Container-Partition': '3',
@@ -226,7 +238,8 @@
226 timestamp + '.data')238 timestamp + '.data')
227 self.assert_(os.path.isfile(objfile))239 self.assert_(os.path.isfile(objfile))
228 self.assertEquals(open(objfile).read(), 'VERIFY')240 self.assertEquals(open(objfile).read(), 'VERIFY')
229 self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)),241 self.assertEquals(pickle.loads(getxattr(objfile,
242 object_server.METADATA_KEY)),
230 {'X-Timestamp': timestamp,243 {'X-Timestamp': timestamp,
231 'Content-Length': '6',244 'Content-Length': '6',
232 'ETag': '0b4c12d7e0a73840c1c4f148fda3b037',245 'ETag': '0b4c12d7e0a73840c1c4f148fda3b037',
@@ -258,7 +271,8 @@
258 timestamp + '.data')271 timestamp + '.data')
259 self.assert_(os.path.isfile(objfile))272 self.assert_(os.path.isfile(objfile))
260 self.assertEquals(open(objfile).read(), 'VERIFY TWO')273 self.assertEquals(open(objfile).read(), 'VERIFY TWO')
261 self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)),274 self.assertEquals(pickle.loads(getxattr(objfile,
275 object_server.METADATA_KEY)),
262 {'X-Timestamp': timestamp,276 {'X-Timestamp': timestamp,
263 'Content-Length': '10',277 'Content-Length': '10',
264 'ETag': 'b381a4c5dab1eaa1eb9711fa647cd039',278 'ETag': 'b381a4c5dab1eaa1eb9711fa647cd039',
@@ -270,17 +284,17 @@
270 if not self.path_to_test_xfs:284 if not self.path_to_test_xfs:
271 raise SkipTest285 raise SkipTest
272 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},286 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
273 headers={'X-Timestamp': normalize_timestamp(time()),287 headers={'X-Timestamp': normalize_timestamp(time()),
274 'Content-Type': 'text/plain'})288 'Content-Type': 'text/plain'})
275 req.body = 'test'289 req.body = 'test'
276 resp = self.object_controller.PUT(req)290 resp = self.object_controller.PUT(req)
277 self.assertEquals(resp.status_int, 201)291 self.assertEquals(resp.status_int, 201)
278292
279 def test_PUT_invalid_etag(self):293 def test_PUT_invalid_etag(self):
280 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},294 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
281 headers={'X-Timestamp': normalize_timestamp(time()),295 headers={'X-Timestamp': normalize_timestamp(time()),
282 'Content-Type': 'text/plain',296 'Content-Type': 'text/plain',
283 'ETag': 'invalid'})297 'ETag': 'invalid'})
284 req.body = 'test'298 req.body = 'test'
285 resp = self.object_controller.PUT(req)299 resp = self.object_controller.PUT(req)
286 self.assertEquals(resp.status_int, 422)300 self.assertEquals(resp.status_int, 422)
@@ -304,7 +318,8 @@
304 timestamp + '.data')318 timestamp + '.data')
305 self.assert_(os.path.isfile(objfile))319 self.assert_(os.path.isfile(objfile))
306 self.assertEquals(open(objfile).read(), 'VERIFY THREE')320 self.assertEquals(open(objfile).read(), 'VERIFY THREE')
307 self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)),321 self.assertEquals(pickle.loads(getxattr(objfile,
322 object_server.METADATA_KEY)),
308 {'X-Timestamp': timestamp,323 {'X-Timestamp': timestamp,
309 'Content-Length': '12',324 'Content-Length': '12',
310 'ETag': 'b114ab7b90d9ccac4bd5d99cc7ebb568',325 'ETag': 'b114ab7b90d9ccac4bd5d99cc7ebb568',
@@ -316,25 +331,33 @@
316 def test_PUT_container_connection(self):331 def test_PUT_container_connection(self):
317 if not self.path_to_test_xfs:332 if not self.path_to_test_xfs:
318 raise SkipTest333 raise SkipTest
334
319 def mock_http_connect(response, with_exc=False):335 def mock_http_connect(response, with_exc=False):
336
320 class FakeConn(object):337 class FakeConn(object):
338
321 def __init__(self, status, with_exc):339 def __init__(self, status, with_exc):
322 self.status = status340 self.status = status
323 self.reason = 'Fake'341 self.reason = 'Fake'
324 self.host = '1.2.3.4'342 self.host = '1.2.3.4'
325 self.port = '1234'343 self.port = '1234'
326 self.with_exc = with_exc344 self.with_exc = with_exc
345
327 def getresponse(self):346 def getresponse(self):
328 if self.with_exc:347 if self.with_exc:
329 raise Exception('test')348 raise Exception('test')
330 return self349 return self
350
331 def read(self, amt=None):351 def read(self, amt=None):
332 return ''352 return ''
353
333 return lambda *args, **kwargs: FakeConn(response, with_exc)354 return lambda *args, **kwargs: FakeConn(response, with_exc)
355
334 old_http_connect = object_server.http_connect356 old_http_connect = object_server.http_connect
335 try:357 try:
336 timestamp = normalize_timestamp(time())358 timestamp = normalize_timestamp(time())
337 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'},359 req = Request.blank('/sda1/p/a/c/o',
360 environ={'REQUEST_METHOD': 'POST'},
338 headers={'X-Timestamp': timestamp,361 headers={'X-Timestamp': timestamp,
339 'X-Container-Host': '1.2.3.4:0',362 'X-Container-Host': '1.2.3.4:0',
340 'X-Container-Partition': '3',363 'X-Container-Partition': '3',
@@ -555,7 +578,8 @@
555 self.assertEquals(resp.status_int, 200)578 self.assertEquals(resp.status_int, 200)
556 self.assertEquals(resp.etag, etag)579 self.assertEquals(resp.etag, etag)
557580
558 req = Request.blank('/sda1/p/a/c/o2', environ={'REQUEST_METHOD': 'GET'},581 req = Request.blank('/sda1/p/a/c/o2',
582 environ={'REQUEST_METHOD': 'GET'},
559 headers={'If-Match': '*'})583 headers={'If-Match': '*'})
560 resp = self.object_controller.GET(req)584 resp = self.object_controller.GET(req)
561 self.assertEquals(resp.status_int, 412)585 self.assertEquals(resp.status_int, 412)
@@ -715,7 +739,8 @@
715 """ Test swift.object_server.ObjectController.DELETE """739 """ Test swift.object_server.ObjectController.DELETE """
716 if not self.path_to_test_xfs:740 if not self.path_to_test_xfs:
717 raise SkipTest741 raise SkipTest
718 req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'DELETE'})742 req = Request.blank('/sda1/p/a/c',
743 environ={'REQUEST_METHOD': 'DELETE'})
719 resp = self.object_controller.DELETE(req)744 resp = self.object_controller.DELETE(req)
720 self.assertEquals(resp.status_int, 400)745 self.assertEquals(resp.status_int, 400)
721746
@@ -916,21 +941,26 @@
916 def test_disk_file_mkstemp_creates_dir(self):941 def test_disk_file_mkstemp_creates_dir(self):
917 tmpdir = os.path.join(self.testdir, 'sda1', 'tmp')942 tmpdir = os.path.join(self.testdir, 'sda1', 'tmp')
918 os.rmdir(tmpdir)943 os.rmdir(tmpdir)
919 with object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o').mkstemp():944 with object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c',
945 'o').mkstemp():
920 self.assert_(os.path.exists(tmpdir))946 self.assert_(os.path.exists(tmpdir))
921947
922 def test_max_upload_time(self):948 def test_max_upload_time(self):
923 if not self.path_to_test_xfs:949 if not self.path_to_test_xfs:
924 raise SkipTest950 raise SkipTest
951
925 class SlowBody():952 class SlowBody():
953
926 def __init__(self):954 def __init__(self):
927 self.sent = 0955 self.sent = 0
956
928 def read(self, size=-1):957 def read(self, size=-1):
929 if self.sent < 4:958 if self.sent < 4:
930 sleep(0.1)959 sleep(0.1)
931 self.sent += 1960 self.sent += 1
932 return ' '961 return ' '
933 return ''962 return ''
963
934 req = Request.blank('/sda1/p/a/c/o',964 req = Request.blank('/sda1/p/a/c/o',
935 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},965 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},
936 headers={'X-Timestamp': normalize_timestamp(time()),966 headers={'X-Timestamp': normalize_timestamp(time()),
@@ -946,14 +976,18 @@
946 self.assertEquals(resp.status_int, 408)976 self.assertEquals(resp.status_int, 408)
947977
948 def test_short_body(self):978 def test_short_body(self):
979
949 class ShortBody():980 class ShortBody():
981
950 def __init__(self):982 def __init__(self):
951 self.sent = False983 self.sent = False
984
952 def read(self, size=-1):985 def read(self, size=-1):
953 if not self.sent:986 if not self.sent:
954 self.sent = True987 self.sent = True
955 return ' '988 return ' '
956 return ''989 return ''
990
957 req = Request.blank('/sda1/p/a/c/o',991 req = Request.blank('/sda1/p/a/c/o',
958 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': ShortBody()},992 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': ShortBody()},
959 headers={'X-Timestamp': normalize_timestamp(time()),993 headers={'X-Timestamp': normalize_timestamp(time()),
@@ -1001,11 +1035,37 @@
1001 resp = self.object_controller.GET(req)1035 resp = self.object_controller.GET(req)
1002 self.assertEquals(resp.status_int, 200)1036 self.assertEquals(resp.status_int, 200)
1003 self.assertEquals(resp.headers['content-encoding'], 'gzip')1037 self.assertEquals(resp.headers['content-encoding'], 'gzip')
1004 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'HEAD'})1038 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD':
1039 'HEAD'})
1005 resp = self.object_controller.HEAD(req)1040 resp = self.object_controller.HEAD(req)
1006 self.assertEquals(resp.status_int, 200)1041 self.assertEquals(resp.status_int, 200)
1007 self.assertEquals(resp.headers['content-encoding'], 'gzip')1042 self.assertEquals(resp.headers['content-encoding'], 'gzip')
10081043
1044 def test_manifest_header(self):
1045 if not self.path_to_test_xfs:
1046 raise SkipTest
1047 timestamp = normalize_timestamp(time())
1048 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
1049 headers={'X-Timestamp': timestamp,
1050 'Content-Type': 'text/plain',
1051 'Content-Length': '0',
1052 'X-Object-Manifest': 'c/o/'})
1053 resp = self.object_controller.PUT(req)
1054 self.assertEquals(resp.status_int, 201)
1055 objfile = os.path.join(self.testdir, 'sda1',
1056 storage_directory(object_server.DATADIR, 'p', hash_path('a', 'c',
1057 'o')), timestamp + '.data')
1058 self.assert_(os.path.isfile(objfile))
1059 self.assertEquals(pickle.loads(getxattr(objfile,
1060 object_server.METADATA_KEY)), {'X-Timestamp': timestamp,
1061 'Content-Length': '0', 'Content-Type': 'text/plain', 'name':
1062 '/a/c/o', 'X-Object-Manifest': 'c/o/', 'ETag':
1063 'd41d8cd98f00b204e9800998ecf8427e'})
1064 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'})
1065 resp = self.object_controller.GET(req)
1066 self.assertEquals(resp.status_int, 200)
1067 self.assertEquals(resp.headers.get('x-object-manifest'), 'c/o/')
1068
10091069
1010if __name__ == '__main__':1070if __name__ == '__main__':
1011 unittest.main()1071 unittest.main()
10121072
=== modified file 'test/unit/proxy/test_server.py'
--- test/unit/proxy/test_server.py 2010-12-06 20:02:43 +0000
+++ test/unit/proxy/test_server.py 2010-12-13 22:17:44 +0000
@@ -35,8 +35,8 @@
35from eventlet import sleep, spawn, TimeoutError, util, wsgi, listen35from eventlet import sleep, spawn, TimeoutError, util, wsgi, listen
36from eventlet.timeout import Timeout36from eventlet.timeout import Timeout
37import simplejson37import simplejson
38from webob import Request38from webob import Request, Response
39from webob.exc import HTTPUnauthorized39from webob.exc import HTTPNotFound, HTTPUnauthorized
4040
41from test.unit import connect_tcp, readuntil2crlfs41from test.unit import connect_tcp, readuntil2crlfs
42from swift.proxy import server as proxy_server42from swift.proxy import server as proxy_server
@@ -53,7 +53,9 @@
5353
5454
55def fake_http_connect(*code_iter, **kwargs):55def fake_http_connect(*code_iter, **kwargs):
56
56 class FakeConn(object):57 class FakeConn(object):
58
57 def __init__(self, status, etag=None, body=''):59 def __init__(self, status, etag=None, body=''):
58 self.status = status60 self.status = status
59 self.reason = 'Fake'61 self.reason = 'Fake'
@@ -160,6 +162,7 @@
160162
161163
162class FakeMemcache(object):164class FakeMemcache(object):
165
163 def __init__(self):166 def __init__(self):
164 self.store = {}167 self.store = {}
165168
@@ -372,9 +375,12 @@
372class TestProxyServer(unittest.TestCase):375class TestProxyServer(unittest.TestCase):
373376
374 def test_unhandled_exception(self):377 def test_unhandled_exception(self):
378
375 class MyApp(proxy_server.Application):379 class MyApp(proxy_server.Application):
380
376 def get_controller(self, path):381 def get_controller(self, path):
377 raise Exception('this shouldnt be caught')382 raise Exception('this shouldnt be caught')
383
378 app = MyApp(None, FakeMemcache(), account_ring=FakeRing(),384 app = MyApp(None, FakeMemcache(), account_ring=FakeRing(),
379 container_ring=FakeRing(), object_ring=FakeRing())385 container_ring=FakeRing(), object_ring=FakeRing())
380 req = Request.blank('/account', environ={'REQUEST_METHOD': 'HEAD'})386 req = Request.blank('/account', environ={'REQUEST_METHOD': 'HEAD'})
@@ -497,8 +503,11 @@
497 test_status_map((200, 200, 204, 500, 404), 503)503 test_status_map((200, 200, 204, 500, 404), 503)
498504
499 def test_PUT_connect_exceptions(self):505 def test_PUT_connect_exceptions(self):
506
500 def mock_http_connect(*code_iter, **kwargs):507 def mock_http_connect(*code_iter, **kwargs):
508
501 class FakeConn(object):509 class FakeConn(object):
510
502 def __init__(self, status):511 def __init__(self, status):
503 self.status = status512 self.status = status
504 self.reason = 'Fake'513 self.reason = 'Fake'
@@ -518,6 +527,7 @@
518 if self.status == -3:527 if self.status == -3:
519 return FakeConn(507)528 return FakeConn(507)
520 return FakeConn(100)529 return FakeConn(100)
530
521 code_iter = iter(code_iter)531 code_iter = iter(code_iter)
522532
523 def connect(*args, **ckwargs):533 def connect(*args, **ckwargs):
@@ -525,7 +535,9 @@
525 if status == -1:535 if status == -1:
526 raise HTTPException()536 raise HTTPException()
527 return FakeConn(status)537 return FakeConn(status)
538
528 return connect539 return connect
540
529 with save_globals():541 with save_globals():
530 controller = proxy_server.ObjectController(self.app, 'account',542 controller = proxy_server.ObjectController(self.app, 'account',
531 'container', 'object')543 'container', 'object')
@@ -546,8 +558,11 @@
546 test_status_map((200, 200, 503, 503, -1), 503)558 test_status_map((200, 200, 503, 503, -1), 503)
547559
548 def test_PUT_send_exceptions(self):560 def test_PUT_send_exceptions(self):
561
549 def mock_http_connect(*code_iter, **kwargs):562 def mock_http_connect(*code_iter, **kwargs):
563
550 class FakeConn(object):564 class FakeConn(object):
565
551 def __init__(self, status):566 def __init__(self, status):
552 self.status = status567 self.status = status
553 self.reason = 'Fake'568 self.reason = 'Fake'
@@ -611,8 +626,11 @@
611 self.assertEquals(res.status_int, 413)626 self.assertEquals(res.status_int, 413)
612627
613 def test_PUT_getresponse_exceptions(self):628 def test_PUT_getresponse_exceptions(self):
629
614 def mock_http_connect(*code_iter, **kwargs):630 def mock_http_connect(*code_iter, **kwargs):
631
615 class FakeConn(object):632 class FakeConn(object):
633
616 def __init__(self, status):634 def __init__(self, status):
617 self.status = status635 self.status = status
618 self.reason = 'Fake'636 self.reason = 'Fake'
@@ -807,6 +825,7 @@
807 dev['port'] = 1825 dev['port'] = 1
808826
809 class SlowBody():827 class SlowBody():
828
810 def __init__(self):829 def __init__(self):
811 self.sent = 0830 self.sent = 0
812831
@@ -816,6 +835,7 @@
816 self.sent += 1835 self.sent += 1
817 return ' '836 return ' '
818 return ''837 return ''
838
819 req = Request.blank('/a/c/o',839 req = Request.blank('/a/c/o',
820 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},840 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},
821 headers={'Content-Length': '4', 'Content-Type': 'text/plain'})841 headers={'Content-Length': '4', 'Content-Type': 'text/plain'})
@@ -854,11 +874,13 @@
854 dev['port'] = 1874 dev['port'] = 1
855875
856 class SlowBody():876 class SlowBody():
877
857 def __init__(self):878 def __init__(self):
858 self.sent = 0879 self.sent = 0
859880
860 def read(self, size=-1):881 def read(self, size=-1):
861 raise Exception('Disconnected')882 raise Exception('Disconnected')
883
862 req = Request.blank('/a/c/o',884 req = Request.blank('/a/c/o',
863 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},885 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()},
864 headers={'Content-Length': '4', 'Content-Type': 'text/plain'})886 headers={'Content-Length': '4', 'Content-Type': 'text/plain'})
@@ -1508,7 +1530,9 @@
15081530
1509 def test_chunked_put(self):1531 def test_chunked_put(self):
1510 # quick test of chunked put w/o PATH_TO_TEST_XFS1532 # quick test of chunked put w/o PATH_TO_TEST_XFS
1533
1511 class ChunkedFile():1534 class ChunkedFile():
1535
1512 def __init__(self, bytes):1536 def __init__(self, bytes):
1513 self.bytes = bytes1537 self.bytes = bytes
1514 self.read_bytes = 01538 self.read_bytes = 0
@@ -1576,6 +1600,7 @@
1576 mkdirs(os.path.join(testdir, 'sdb1'))1600 mkdirs(os.path.join(testdir, 'sdb1'))
1577 mkdirs(os.path.join(testdir, 'sdb1', 'tmp'))1601 mkdirs(os.path.join(testdir, 'sdb1', 'tmp'))
1578 try:1602 try:
1603 orig_container_listing_limit = proxy_server.CONTAINER_LISTING_LIMIT
1579 conf = {'devices': testdir, 'swift_dir': testdir,1604 conf = {'devices': testdir, 'swift_dir': testdir,
1580 'mount_check': 'false'}1605 'mount_check': 'false'}
1581 prolis = listen(('localhost', 0))1606 prolis = listen(('localhost', 0))
@@ -1669,8 +1694,10 @@
1669 self.assertEquals(headers[:len(exp)], exp)1694 self.assertEquals(headers[:len(exp)], exp)
1670 # Check unhandled exception1695 # Check unhandled exception
1671 orig_update_request = prosrv.update_request1696 orig_update_request = prosrv.update_request
1697
1672 def broken_update_request(env, req):1698 def broken_update_request(env, req):
1673 raise Exception('fake')1699 raise Exception('fake')
1700
1674 prosrv.update_request = broken_update_request1701 prosrv.update_request = broken_update_request
1675 sock = connect_tcp(('localhost', prolis.getsockname()[1]))1702 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1676 fd = sock.makefile()1703 fd = sock.makefile()
@@ -1719,8 +1746,10 @@
1719 # in a test for logging x-forwarded-for (first entry only).1746 # in a test for logging x-forwarded-for (first entry only).
17201747
1721 class Logger(object):1748 class Logger(object):
1749
1722 def info(self, msg):1750 def info(self, msg):
1723 self.msg = msg1751 self.msg = msg
1752
1724 orig_logger = prosrv.logger1753 orig_logger = prosrv.logger
1725 prosrv.logger = Logger()1754 prosrv.logger = Logger()
1726 sock = connect_tcp(('localhost', prolis.getsockname()[1]))1755 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
@@ -1742,8 +1771,10 @@
1742 # Turn on header logging.1771 # Turn on header logging.
17431772
1744 class Logger(object):1773 class Logger(object):
1774
1745 def info(self, msg):1775 def info(self, msg):
1746 self.msg = msg1776 self.msg = msg
1777
1747 orig_logger = prosrv.logger1778 orig_logger = prosrv.logger
1748 prosrv.logger = Logger()1779 prosrv.logger = Logger()
1749 prosrv.log_headers = True1780 prosrv.log_headers = True
@@ -1900,6 +1931,70 @@
1900 self.assertEquals(headers[:len(exp)], exp)1931 self.assertEquals(headers[:len(exp)], exp)
1901 body = fd.read()1932 body = fd.read()
1902 self.assertEquals(body, 'oh hai123456789abcdef')1933 self.assertEquals(body, 'oh hai123456789abcdef')
1934 # Create a container for our segmented/manifest object testing
1935 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1936 fd = sock.makefile()
1937 fd.write('PUT /v1/a/segmented HTTP/1.1\r\nHost: localhost\r\n'
1938 'Connection: close\r\nX-Storage-Token: t\r\n'
1939 'Content-Length: 0\r\n\r\n')
1940 fd.flush()
1941 headers = readuntil2crlfs(fd)
1942 exp = 'HTTP/1.1 201'
1943 self.assertEquals(headers[:len(exp)], exp)
1944 # Create the object segments
1945 for segment in xrange(5):
1946 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1947 fd = sock.makefile()
1948 fd.write('PUT /v1/a/segmented/name/%s HTTP/1.1\r\nHost: '
1949 'localhost\r\nConnection: close\r\nX-Storage-Token: '
1950 't\r\nContent-Length: 5\r\n\r\n1234 ' % str(segment))
1951 fd.flush()
1952 headers = readuntil2crlfs(fd)
1953 exp = 'HTTP/1.1 201'
1954 self.assertEquals(headers[:len(exp)], exp)
1955 # Create the object manifest file
1956 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1957 fd = sock.makefile()
1958 fd.write('PUT /v1/a/segmented/name HTTP/1.1\r\nHost: '
1959 'localhost\r\nConnection: close\r\nX-Storage-Token: '
1960 't\r\nContent-Length: 0\r\nX-Object-Manifest: '
1961 'segmented/name/\r\nContent-Type: text/jibberish\r\n\r\n')
1962 fd.flush()
1963 headers = readuntil2crlfs(fd)
1964 exp = 'HTTP/1.1 201'
1965 self.assertEquals(headers[:len(exp)], exp)
1966 # Ensure retrieving the manifest file gets the whole object
1967 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1968 fd = sock.makefile()
1969 fd.write('GET /v1/a/segmented/name HTTP/1.1\r\nHost: '
1970 'localhost\r\nConnection: close\r\nX-Auth-Token: '
1971 't\r\n\r\n')
1972 fd.flush()
1973 headers = readuntil2crlfs(fd)
1974 exp = 'HTTP/1.1 200'
1975 self.assertEquals(headers[:len(exp)], exp)
1976 self.assert_('X-Object-Manifest: segmented/name/' in headers)
1977 self.assert_('Content-Type: text/jibberish' in headers)
1978 body = fd.read()
1979 self.assertEquals(body, '1234 1234 1234 1234 1234 ')
1980 # Do it again but exceeding the container listing limit
1981 proxy_server.CONTAINER_LISTING_LIMIT = 2
1982 sock = connect_tcp(('localhost', prolis.getsockname()[1]))
1983 fd = sock.makefile()
1984 fd.write('GET /v1/a/segmented/name HTTP/1.1\r\nHost: '
1985 'localhost\r\nConnection: close\r\nX-Auth-Token: '
1986 't\r\n\r\n')
1987 fd.flush()
1988 headers = readuntil2crlfs(fd)
1989 exp = 'HTTP/1.1 200'
1990 self.assertEquals(headers[:len(exp)], exp)
1991 self.assert_('X-Object-Manifest: segmented/name/' in headers)
1992 self.assert_('Content-Type: text/jibberish' in headers)
1993 body = fd.read()
1994 # A bit fragile of a test; as it makes the assumption that all
1995 # will be sent in a single chunk.
1996 self.assertEquals(body,
1997 '19\r\n1234 1234 1234 1234 1234 \r\n0\r\n\r\n')
1903 finally:1998 finally:
1904 prospa.kill()1999 prospa.kill()
1905 acc1spa.kill()2000 acc1spa.kill()
@@ -1909,6 +2004,7 @@
1909 obj1spa.kill()2004 obj1spa.kill()
1910 obj2spa.kill()2005 obj2spa.kill()
1911 finally:2006 finally:
2007 proxy_server.CONTAINER_LISTING_LIMIT = orig_container_listing_limit
1912 rmtree(testdir)2008 rmtree(testdir)
19132009
1914 def test_mismatched_etags(self):2010 def test_mismatched_etags(self):
@@ -2111,6 +2207,7 @@
2111 res = controller.COPY(req)2207 res = controller.COPY(req)
2112 self.assert_(called[0])2208 self.assert_(called[0])
21132209
2210
2114class TestContainerController(unittest.TestCase):2211class TestContainerController(unittest.TestCase):
2115 "Test swift.proxy_server.ContainerController"2212 "Test swift.proxy_server.ContainerController"
21162213
@@ -2254,7 +2351,9 @@
2254 self.assertEquals(resp.status_int, 404)2351 self.assertEquals(resp.status_int, 404)
22552352
2256 def test_put_locking(self):2353 def test_put_locking(self):
2354
2257 class MockMemcache(FakeMemcache):2355 class MockMemcache(FakeMemcache):
2356
2258 def __init__(self, allow_lock=None):2357 def __init__(self, allow_lock=None):
2259 self.allow_lock = allow_lock2358 self.allow_lock = allow_lock
2260 super(MockMemcache, self).__init__()2359 super(MockMemcache, self).__init__()
@@ -2265,6 +2364,7 @@
2265 yield True2364 yield True
2266 else:2365 else:
2267 raise MemcacheLockError()2366 raise MemcacheLockError()
2367
2268 with save_globals():2368 with save_globals():
2269 controller = proxy_server.ContainerController(self.app, 'account',2369 controller = proxy_server.ContainerController(self.app, 'account',
2270 'container')2370 'container')
@@ -2870,5 +2970,261 @@
2870 test_status_map((204, 500, 404), 503)2970 test_status_map((204, 500, 404), 503)
28712971
28722972
2973class FakeObjectController(object):
2974
2975 def __init__(self):
2976 self.app = self
2977 self.logger = self
2978 self.account_name = 'a'
2979 self.container_name = 'c'
2980 self.object_name = 'o'
2981 self.trans_id = 'tx1'
2982 self.object_ring = FakeRing()
2983 self.node_timeout = 1
2984
2985 def exception(self, *args):
2986 self.exception_args = args
2987 self.exception_info = sys.exc_info()
2988
2989 def GETorHEAD_base(self, *args):
2990 self.GETorHEAD_base_args = args
2991 req = args[0]
2992 path = args[4]
2993 body = data = path[-1] * int(path[-1])
2994 if req.range and req.range.ranges:
2995 body = ''
2996 for start, stop in req.range.ranges:
2997 body += data[start:stop]
2998 resp = Response(app_iter=iter(body))
2999 return resp
3000
3001 def iter_nodes(self, partition, nodes, ring):
3002 for node in nodes:
3003 yield node
3004 for node in ring.get_more_nodes(partition):
3005 yield node
3006
3007
3008class Stub(object):
3009 pass
3010
3011
3012class TestSegmentedIterable(unittest.TestCase):
3013
3014 def setUp(self):
3015 self.controller = FakeObjectController()
3016
3017 def test_load_next_segment_unexpected_error(self):
3018 # Iterator value isn't a dict
3019 self.assertRaises(Exception,
3020 proxy_server.SegmentedIterable(self.controller, None,
3021 [None])._load_next_segment)
3022 self.assertEquals(self.controller.exception_args[0],
3023 'ERROR: While processing manifest /a/c/o tx1')
3024
3025 def test_load_next_segment_with_no_segments(self):
3026 self.assertRaises(StopIteration,
3027 proxy_server.SegmentedIterable(self.controller, 'lc',
3028 [])._load_next_segment)
3029
3030 def test_load_next_segment_with_one_segment(self):
3031 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3032 'o1'}])
3033 segit._load_next_segment()
3034 self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1')
3035 data = ''.join(segit.segment_iter)
3036 self.assertEquals(data, '1')
3037
3038 def test_load_next_segment_with_two_segments(self):
3039 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3040 'o1'}, {'name': 'o2'}])
3041 segit._load_next_segment()
3042 self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1')
3043 data = ''.join(segit.segment_iter)
3044 self.assertEquals(data, '1')
3045 segit._load_next_segment()
3046 self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2')
3047 data = ''.join(segit.segment_iter)
3048 self.assertEquals(data, '22')
3049
3050 def test_load_next_segment_with_two_segments_skip_first(self):
3051 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3052 'o1'}, {'name': 'o2'}])
3053 segit.segment = 0
3054 segit.listing.next()
3055 segit._load_next_segment()
3056 self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2')
3057 data = ''.join(segit.segment_iter)
3058 self.assertEquals(data, '22')
3059
3060 def test_load_next_segment_with_seek(self):
3061 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3062 'o1'}, {'name': 'o2'}])
3063 segit.segment = 0
3064 segit.listing.next()
3065 segit.seek = 1
3066 segit._load_next_segment()
3067 self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2')
3068 self.assertEquals(str(self.controller.GETorHEAD_base_args[0].range),
3069 'bytes=1-')
3070 data = ''.join(segit.segment_iter)
3071 self.assertEquals(data, '2')
3072
3073 def test_load_next_segment_with_get_error(self):
3074
3075 def local_GETorHEAD_base(*args):
3076 return HTTPNotFound()
3077
3078 self.controller.GETorHEAD_base = local_GETorHEAD_base
3079 self.assertRaises(Exception,
3080 proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3081 'o1'}])._load_next_segment)
3082 self.assertEquals(self.controller.exception_args[0],
3083 'ERROR: While processing manifest /a/c/o tx1')
3084 self.assertEquals(str(self.controller.exception_info[1]),
3085 'Could not load object segment /a/lc/o1: 404')
3086
3087 def test_iter_unexpected_error(self):
3088 # Iterator value isn't a dict
3089 self.assertRaises(Exception, ''.join,
3090 proxy_server.SegmentedIterable(self.controller, None, [None]))
3091 self.assertEquals(self.controller.exception_args[0],
3092 'ERROR: While processing manifest /a/c/o tx1')
3093
3094 def test_iter_with_no_segments(self):
3095 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [])
3096 self.assertEquals(''.join(segit), '')
3097
3098 def test_iter_with_one_segment(self):
3099 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3100 'o1'}])
3101 segit.response = Stub()
3102 self.assertEquals(''.join(segit), '1')
3103 self.assertEquals(segit.response.bytes_transferred, 1)
3104
3105 def test_iter_with_two_segments(self):
3106 segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3107 'o1'}, {'name': 'o2'}])
3108 segit.response = Stub()
3109 self.assertEquals(''.join(segit), '122')
3110 self.assertEquals(segit.response.bytes_transferred, 3)
3111
3112 def test_iter_with_get_error(self):
3113
3114 def local_GETorHEAD_base(*args):
3115 return HTTPNotFound()
3116
3117 self.controller.GETorHEAD_base = local_GETorHEAD_base
3118 self.assertRaises(Exception, ''.join,
3119 proxy_server.SegmentedIterable(self.controller, 'lc', [{'name':
3120 'o1'}]))
3121 self.assertEquals(self.controller.exception_args[0],
3122 'ERROR: While processing manifest /a/c/o tx1')
3123 self.assertEquals(str(self.controller.exception_info[1]),
3124 'Could not load object segment /a/lc/o1: 404')
3125
3126 def test_app_iter_range_unexpected_error(self):
3127 # Iterator value isn't a dict
3128 self.assertRaises(Exception,
3129 proxy_server.SegmentedIterable(self.controller, None,
3130 [None]).app_iter_range(None, None).next)
3131 self.assertEquals(self.controller.exception_args[0],
3132 'ERROR: While processing manifest /a/c/o tx1')
3133
3134 def test_app_iter_range_with_no_segments(self):
3135 self.assertEquals(''.join(proxy_server.SegmentedIterable(
3136 self.controller, 'lc', []).app_iter_range(None, None)), '')
3137 self.assertEquals(''.join(proxy_server.SegmentedIterable(
3138 self.controller, 'lc', []).app_iter_range(3, None)), '')
3139 self.assertEquals(''.join(proxy_server.SegmentedIterable(
3140 self.controller, 'lc', []).app_iter_range(3, 5)), '')
3141 self.assertEquals(''.join(proxy_server.SegmentedIterable(
3142 self.controller, 'lc', []).app_iter_range(None, 5)), '')
3143
3144 def test_app_iter_range_with_one_segment(self):
3145 listing = [{'name': 'o1', 'bytes': 1}]
3146
3147 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3148 segit.response = Stub()
3149 self.assertEquals(''.join(segit.app_iter_range(None, None)), '1')
3150 self.assertEquals(segit.response.bytes_transferred, 1)
3151
3152 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3153 self.assertEquals(''.join(segit.app_iter_range(3, None)), '')
3154
3155 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3156 self.assertEquals(''.join(segit.app_iter_range(3, 5)), '')
3157
3158 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3159 segit.response = Stub()
3160 self.assertEquals(''.join(segit.app_iter_range(None, 5)), '1')
3161 self.assertEquals(segit.response.bytes_transferred, 1)
3162
3163 def test_app_iter_range_with_two_segments(self):
3164 listing = [{'name': 'o1', 'bytes': 1}, {'name': 'o2', 'bytes': 2}]
3165
3166 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3167 segit.response = Stub()
3168 self.assertEquals(''.join(segit.app_iter_range(None, None)), '122')
3169 self.assertEquals(segit.response.bytes_transferred, 3)
3170
3171 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3172 segit.response = Stub()
3173 self.assertEquals(''.join(segit.app_iter_range(1, None)), '22')
3174 self.assertEquals(segit.response.bytes_transferred, 2)
3175
3176 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3177 segit.response = Stub()
3178 self.assertEquals(''.join(segit.app_iter_range(1, 5)), '22')
3179 self.assertEquals(segit.response.bytes_transferred, 2)
3180
3181 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3182 segit.response = Stub()
3183 self.assertEquals(''.join(segit.app_iter_range(None, 2)), '12')
3184 self.assertEquals(segit.response.bytes_transferred, 2)
3185
3186 def test_app_iter_range_with_many_segments(self):
3187 listing = [{'name': 'o1', 'bytes': 1}, {'name': 'o2', 'bytes': 2},
3188 {'name': 'o3', 'bytes': 3}, {'name': 'o4', 'bytes': 4}, {'name':
3189 'o5', 'bytes': 5}]
3190
3191 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3192 segit.response = Stub()
3193 self.assertEquals(''.join(segit.app_iter_range(None, None)),
3194 '122333444455555')
3195 self.assertEquals(segit.response.bytes_transferred, 15)
3196
3197 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3198 segit.response = Stub()
3199 self.assertEquals(''.join(segit.app_iter_range(3, None)),
3200 '333444455555')
3201 self.assertEquals(segit.response.bytes_transferred, 12)
3202
3203 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3204 segit.response = Stub()
3205 self.assertEquals(''.join(segit.app_iter_range(5, None)), '3444455555')
3206 self.assertEquals(segit.response.bytes_transferred, 10)
3207
3208 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3209 segit.response = Stub()
3210 self.assertEquals(''.join(segit.app_iter_range(None, 6)), '122333')
3211 self.assertEquals(segit.response.bytes_transferred, 6)
3212
3213 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3214 segit.response = Stub()
3215 self.assertEquals(''.join(segit.app_iter_range(None, 7)), '1223334')
3216 self.assertEquals(segit.response.bytes_transferred, 7)
3217
3218 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3219 segit.response = Stub()
3220 self.assertEquals(''.join(segit.app_iter_range(3, 7)), '3334')
3221 self.assertEquals(segit.response.bytes_transferred, 4)
3222
3223 segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing)
3224 segit.response = Stub()
3225 self.assertEquals(''.join(segit.app_iter_range(5, 7)), '34')
3226 self.assertEquals(segit.response.bytes_transferred, 2)
3227
3228
2873if __name__ == '__main__':3229if __name__ == '__main__':
2874 unittest.main()3230 unittest.main()