swift 2.32.0__py2.py3-none-any.whl → 2.34.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swift/account/auditor.py +11 -0
- swift/account/reaper.py +11 -1
- swift/account/replicator.py +22 -0
- swift/account/server.py +13 -12
- swift-2.32.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
- swift-2.32.0.data/scripts/swift-config → swift/cli/config.py +1 -1
- swift-2.32.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
- swift-2.32.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
- swift-2.32.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
- swift/cli/info.py +131 -3
- swift-2.32.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
- swift-2.32.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
- swift-2.32.0.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +9 -18
- swift-2.32.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +1 -1
- swift/cli/reload.py +141 -0
- swift/cli/ringbuilder.py +24 -0
- swift/common/daemon.py +12 -2
- swift/common/db.py +14 -9
- swift/common/db_auditor.py +2 -2
- swift/common/db_replicator.py +6 -0
- swift/common/exceptions.py +12 -0
- swift/common/http_protocol.py +76 -3
- swift/common/manager.py +120 -5
- swift/common/memcached.py +24 -25
- swift/common/middleware/account_quotas.py +144 -43
- swift/common/middleware/backend_ratelimit.py +166 -24
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +3 -5
- swift/common/middleware/container_sync.py +6 -10
- swift/common/middleware/crypto/crypto_utils.py +4 -5
- swift/common/middleware/crypto/decrypter.py +4 -5
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/proxy_logging.py +57 -43
- swift/common/middleware/ratelimit.py +6 -7
- swift/common/middleware/recon.py +6 -7
- swift/common/middleware/s3api/acl_handlers.py +10 -1
- swift/common/middleware/s3api/controllers/__init__.py +3 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_upload.py +31 -15
- swift/common/middleware/s3api/controllers/obj.py +20 -1
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/s3api.py +6 -0
- swift/common/middleware/s3api/s3request.py +190 -74
- swift/common/middleware/s3api/s3response.py +48 -8
- swift/common/middleware/s3api/s3token.py +2 -2
- swift/common/middleware/s3api/utils.py +2 -1
- swift/common/middleware/slo.py +508 -310
- swift/common/middleware/staticweb.py +45 -14
- swift/common/middleware/tempauth.py +6 -4
- swift/common/middleware/tempurl.py +134 -93
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +9 -10
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +1 -2
- swift/common/request_helpers.py +101 -8
- swift/common/statsd_client.py +207 -0
- swift/common/storage_policy.py +1 -1
- swift/common/swob.py +5 -2
- swift/common/utils/__init__.py +331 -1774
- swift/common/utils/base.py +138 -0
- swift/common/utils/config.py +443 -0
- swift/common/utils/logs.py +999 -0
- swift/common/utils/timestamp.py +23 -2
- swift/common/wsgi.py +19 -3
- swift/container/auditor.py +11 -0
- swift/container/backend.py +136 -31
- swift/container/reconciler.py +11 -2
- swift/container/replicator.py +64 -7
- swift/container/server.py +276 -146
- swift/container/sharder.py +86 -42
- swift/container/sync.py +11 -1
- swift/container/updater.py +12 -2
- swift/obj/auditor.py +20 -3
- swift/obj/diskfile.py +63 -25
- swift/obj/expirer.py +154 -47
- swift/obj/mem_diskfile.py +2 -1
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +28 -4
- swift/obj/replicator.py +63 -24
- swift/obj/server.py +76 -59
- swift/obj/updater.py +12 -2
- swift/obj/watchers/dark_data.py +72 -34
- swift/proxy/controllers/account.py +3 -2
- swift/proxy/controllers/base.py +254 -148
- swift/proxy/controllers/container.py +274 -289
- swift/proxy/controllers/obj.py +120 -166
- swift/proxy/server.py +17 -13
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +14 -4
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/METADATA +9 -7
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/RECORD +97 -120
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +39 -0
- swift-2.34.0.dist-info/pbr.json +1 -0
- swift-2.32.0.data/scripts/swift-account-auditor +0 -23
- swift-2.32.0.data/scripts/swift-account-info +0 -52
- swift-2.32.0.data/scripts/swift-account-reaper +0 -23
- swift-2.32.0.data/scripts/swift-account-replicator +0 -34
- swift-2.32.0.data/scripts/swift-account-server +0 -23
- swift-2.32.0.data/scripts/swift-container-auditor +0 -23
- swift-2.32.0.data/scripts/swift-container-info +0 -56
- swift-2.32.0.data/scripts/swift-container-reconciler +0 -21
- swift-2.32.0.data/scripts/swift-container-replicator +0 -34
- swift-2.32.0.data/scripts/swift-container-server +0 -23
- swift-2.32.0.data/scripts/swift-container-sharder +0 -37
- swift-2.32.0.data/scripts/swift-container-sync +0 -23
- swift-2.32.0.data/scripts/swift-container-updater +0 -23
- swift-2.32.0.data/scripts/swift-dispersion-report +0 -24
- swift-2.32.0.data/scripts/swift-form-signature +0 -20
- swift-2.32.0.data/scripts/swift-init +0 -119
- swift-2.32.0.data/scripts/swift-object-auditor +0 -29
- swift-2.32.0.data/scripts/swift-object-expirer +0 -33
- swift-2.32.0.data/scripts/swift-object-info +0 -60
- swift-2.32.0.data/scripts/swift-object-reconstructor +0 -33
- swift-2.32.0.data/scripts/swift-object-relinker +0 -23
- swift-2.32.0.data/scripts/swift-object-replicator +0 -37
- swift-2.32.0.data/scripts/swift-object-server +0 -27
- swift-2.32.0.data/scripts/swift-object-updater +0 -23
- swift-2.32.0.data/scripts/swift-proxy-server +0 -23
- swift-2.32.0.data/scripts/swift-recon +0 -24
- swift-2.32.0.data/scripts/swift-ring-builder +0 -37
- swift-2.32.0.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.32.0.data/scripts/swift-ring-composer +0 -22
- swift-2.32.0.dist-info/pbr.json +0 -1
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
swift/common/middleware/slo.py
CHANGED
@@ -270,11 +270,27 @@ A GET request with the query parameters::
|
|
270
270
|
will return the contents of the original manifest as it was sent by the client.
|
271
271
|
The main purpose for both calls is solely debugging.
|
272
272
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
273
|
+
A GET request to a manifest object with the query parameter::
|
274
|
+
|
275
|
+
?part-number=<n>
|
276
|
+
|
277
|
+
will return the contents of the ``nth`` segment. Segments are indexed from 1,
|
278
|
+
so ``n`` must be an integer between 1 and the total number of segments in the
|
279
|
+
manifest. The response status will be ``206 Partial Content`` and its headers
|
280
|
+
will include: an ``X-Parts-Count`` header equal to the total number of
|
281
|
+
segments; a ``Content-Length`` header equal to the length of the specified
|
282
|
+
segment; a ``Content-Range`` header describing the byte range of the specified
|
283
|
+
part within the SLO. A HEAD request with a ``part-number`` parameter will also
|
284
|
+
return a response with status ``206 Partial Content`` and the same headers.
|
285
|
+
|
286
|
+
.. note::
|
287
|
+
|
288
|
+
When the manifest object is uploaded you are more or less guaranteed that
|
289
|
+
every segment in the manifest exists and matched the specifications.
|
290
|
+
However, there is nothing that prevents the user from breaking the SLO
|
291
|
+
download by deleting/replacing a segment referenced in the manifest. It is
|
292
|
+
left to the user to use caution in handling the segments.
|
293
|
+
|
278
294
|
|
279
295
|
-----------------------
|
280
296
|
Deleting a Large Object
|
@@ -334,6 +350,7 @@ import time
|
|
334
350
|
import six
|
335
351
|
|
336
352
|
from swift.cli.container_deleter import make_delete_jobs
|
353
|
+
from swift.common.header_key_dict import HeaderKeyDict
|
337
354
|
from swift.common.exceptions import ListingIterError, SegmentError
|
338
355
|
from swift.common.middleware.listing_formats import \
|
339
356
|
MAX_CONTAINER_LISTING_CONTENT_LENGTH
|
@@ -345,15 +362,16 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
|
|
345
362
|
RESPONSE_REASONS, str_to_wsgi, bytes_to_wsgi, wsgi_to_str, wsgi_quote
|
346
363
|
from swift.common.utils import get_logger, config_true_value, \
|
347
364
|
get_valid_utf8_str, override_bytes_from_content_type, split_path, \
|
348
|
-
RateLimitedIterator, quote,
|
349
|
-
LRUCache, StreamingPile, strict_b64decode, Timestamp,
|
365
|
+
RateLimitedIterator, quote, closing_if_possible, \
|
366
|
+
LRUCache, StreamingPile, strict_b64decode, Timestamp, friendly_close, \
|
350
367
|
get_expirer_container, md5
|
351
368
|
from swift.common.registry import register_swift_info
|
352
369
|
from swift.common.request_helpers import SegmentedIterable, \
|
353
370
|
get_sys_meta_prefix, update_etag_is_at_header, resolve_etag_is_at_header, \
|
354
|
-
get_container_update_override_key, update_ignore_range_header
|
371
|
+
get_container_update_override_key, update_ignore_range_header, \
|
372
|
+
get_param, get_valid_part_num
|
355
373
|
from swift.common.constraints import check_utf8, AUTO_CREATE_ACCOUNT_PREFIX
|
356
|
-
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
374
|
+
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
357
375
|
from swift.common.wsgi import WSGIContext, make_subrequest, make_env, \
|
358
376
|
make_pre_authed_request
|
359
377
|
from swift.common.middleware.bulk import get_response_body, \
|
@@ -530,6 +548,183 @@ def parse_and_validate_input(req_body, req_path):
|
|
530
548
|
return parsed_data
|
531
549
|
|
532
550
|
|
551
|
+
def _annotate_segments(segments, logger=None):
|
552
|
+
"""
|
553
|
+
Decode any inlined data and update sub_slo segments bytes from content-type
|
554
|
+
when available; then annotate segment dicts in segments list with
|
555
|
+
'segment_length'.
|
556
|
+
|
557
|
+
N.B. raw_data segments don't have a bytes key and range-segments need to
|
558
|
+
calculate their length from their range key but afterwards all segments
|
559
|
+
dicts will have 'segment_length' representing the length of the segment.
|
560
|
+
"""
|
561
|
+
for seg_dict in segments:
|
562
|
+
if 'data' in seg_dict:
|
563
|
+
seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
|
564
|
+
segment_length = len(seg_dict['raw_data'])
|
565
|
+
else:
|
566
|
+
if config_true_value(seg_dict.get('sub_slo')):
|
567
|
+
override_bytes_from_content_type(
|
568
|
+
seg_dict, logger=logger)
|
569
|
+
seg_range = seg_dict.get('range')
|
570
|
+
if seg_range is not None:
|
571
|
+
# The range is of the form N-M, where N and M are both
|
572
|
+
# positive decimal integers. We know this because this
|
573
|
+
# middleware is the only thing that creates the SLO
|
574
|
+
# manifests stored in the cluster.
|
575
|
+
range_start, range_end = [
|
576
|
+
int(x) for x in seg_range.split('-')]
|
577
|
+
segment_length = (range_end - range_start) + 1
|
578
|
+
else:
|
579
|
+
segment_length = int(seg_dict['bytes'])
|
580
|
+
seg_dict['segment_length'] = segment_length
|
581
|
+
|
582
|
+
|
583
|
+
def calculate_byterange_for_part_num(req, segments, part_num):
|
584
|
+
"""
|
585
|
+
Helper function to calculate the byterange for a part_num response.
|
586
|
+
|
587
|
+
N.B. as a side-effect of calculating the single tuple representing the
|
588
|
+
byterange required for a part_num response this function will also mutate
|
589
|
+
the request's Range header so that swob knows to return 206.
|
590
|
+
|
591
|
+
:param req: the request object
|
592
|
+
:param segments: the list of seg_dicts
|
593
|
+
:param part_num: the part number of the object to return
|
594
|
+
|
595
|
+
:returns: a tuple representing the byterange
|
596
|
+
"""
|
597
|
+
start = 0
|
598
|
+
for seg in segments[:part_num - 1]:
|
599
|
+
start += seg['segment_length']
|
600
|
+
last = start + segments[part_num - 1]['segment_length']
|
601
|
+
# We need to mutate the request's Range header so that swob knows to
|
602
|
+
# handle these partial content requests correctly.
|
603
|
+
req.range = "bytes=%d-%d" % (start, last - 1)
|
604
|
+
return start, last - 1
|
605
|
+
|
606
|
+
|
607
|
+
def calculate_byteranges(req, segments, resp_attrs, part_num):
|
608
|
+
"""
|
609
|
+
Calculate the byteranges based on the request, segments, and part number.
|
610
|
+
|
611
|
+
N.B. as a side-effect of calculating the single tuple representing the
|
612
|
+
byterange required for a part_num response this function will also mutate
|
613
|
+
the request's Range header so that swob knows to return 206.
|
614
|
+
|
615
|
+
:param req: the request object
|
616
|
+
:param segments: the list of seg_dicts
|
617
|
+
:param resp_attrs: the slo response attributes
|
618
|
+
:param part_num: the part number of the object to return
|
619
|
+
|
620
|
+
:returns: a list of tuples representing byteranges
|
621
|
+
"""
|
622
|
+
if req.range:
|
623
|
+
byteranges = [
|
624
|
+
# For some reason, swob.Range.ranges_for_length adds 1 to the
|
625
|
+
# last byte's position.
|
626
|
+
(start, end - 1) for start, end
|
627
|
+
in req.range.ranges_for_length(resp_attrs.slo_size)]
|
628
|
+
elif part_num:
|
629
|
+
byteranges = [
|
630
|
+
calculate_byterange_for_part_num(req, segments, part_num)]
|
631
|
+
else:
|
632
|
+
byteranges = [(0, resp_attrs.slo_size - 1)]
|
633
|
+
|
634
|
+
return byteranges
|
635
|
+
|
636
|
+
|
637
|
+
class RespAttrs(object):
|
638
|
+
"""
|
639
|
+
Encapsulate properties of a GET or HEAD response that are pertinent to
|
640
|
+
handling a potential SLO response.
|
641
|
+
|
642
|
+
Instances of this class are typically constructed using the
|
643
|
+
``from_headers`` method.
|
644
|
+
|
645
|
+
:param is_slo: True if the response appears to be an SLO manifest, False
|
646
|
+
otherwise.
|
647
|
+
:param timestamp: an instance of :class:`~swift.common.utils.Timestamp`.
|
648
|
+
:param manifest_etag: the Etag of the manifest object, or None if
|
649
|
+
``is_slo`` is False.
|
650
|
+
:param slo_etag: the Etag of the SLO.
|
651
|
+
:param slo_size: the size of the SLO.
|
652
|
+
"""
|
653
|
+
def __init__(self, is_slo, timestamp, manifest_etag, slo_etag, slo_size):
|
654
|
+
self.is_slo = bool(is_slo)
|
655
|
+
self.timestamp = Timestamp(timestamp or 0)
|
656
|
+
# manifest_etag is unambiguous, but json_md5 is even more explicit
|
657
|
+
self.json_md5 = manifest_etag or ''
|
658
|
+
self.slo_etag = slo_etag or ''
|
659
|
+
try:
|
660
|
+
# even though it's from sysmeta, we have to worry about empty
|
661
|
+
# values - see test_get_invalid_sysmeta_passthrough
|
662
|
+
self.slo_size = int(slo_size)
|
663
|
+
except (ValueError, TypeError):
|
664
|
+
self.slo_size = -1
|
665
|
+
self.is_legacy = not self._has_size_and_etag()
|
666
|
+
|
667
|
+
def _has_size_and_etag(self):
|
668
|
+
return self.slo_size >= 0 and self.slo_etag
|
669
|
+
|
670
|
+
@classmethod
|
671
|
+
def from_headers(cls, response_headers):
|
672
|
+
"""
|
673
|
+
Inspect response headers and extract any resp_attrs we can find.
|
674
|
+
|
675
|
+
:param response_headers: list of tuples from a object response
|
676
|
+
:returns: an instance of RespAttrs to represent the response headers
|
677
|
+
"""
|
678
|
+
is_slo = False
|
679
|
+
timestamp = None
|
680
|
+
found_etag = None
|
681
|
+
slo_etag = None
|
682
|
+
slo_size = None
|
683
|
+
for header, value in response_headers:
|
684
|
+
header = header.lower()
|
685
|
+
if header == 'x-static-large-object':
|
686
|
+
is_slo = config_true_value(value)
|
687
|
+
elif header == 'x-backend-timestamp':
|
688
|
+
timestamp = value
|
689
|
+
elif header == 'etag':
|
690
|
+
found_etag = value
|
691
|
+
elif header == SYSMETA_SLO_ETAG:
|
692
|
+
slo_etag = value
|
693
|
+
elif header == SYSMETA_SLO_SIZE:
|
694
|
+
slo_size = value
|
695
|
+
manifest_etag = found_etag if is_slo else None
|
696
|
+
return cls(is_slo, timestamp, manifest_etag, slo_etag, slo_size)
|
697
|
+
|
698
|
+
def update_from_segments(self, segments):
|
699
|
+
"""
|
700
|
+
Always called if SLO has fetched the manifest response body, for
|
701
|
+
legacy manifests we'll calculate size/etag values we wouldn't have
|
702
|
+
gotten from sys-meta headers.
|
703
|
+
"""
|
704
|
+
# we only have to set size/etag once; it doesn't matter if we got the
|
705
|
+
# values from sysmeta headers or segments
|
706
|
+
if self._has_size_and_etag():
|
707
|
+
return
|
708
|
+
|
709
|
+
calculated_size = 0
|
710
|
+
calculated_etag = md5(usedforsecurity=False)
|
711
|
+
|
712
|
+
for seg_dict in segments:
|
713
|
+
calculated_size += seg_dict['segment_length']
|
714
|
+
|
715
|
+
if 'raw_data' in seg_dict:
|
716
|
+
r = md5(seg_dict['raw_data'],
|
717
|
+
usedforsecurity=False).hexdigest()
|
718
|
+
elif seg_dict.get('range'):
|
719
|
+
r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
|
720
|
+
else:
|
721
|
+
r = seg_dict['hash']
|
722
|
+
calculated_etag.update(r.encode('ascii'))
|
723
|
+
|
724
|
+
self.slo_size = calculated_size
|
725
|
+
self.slo_etag = calculated_etag.hexdigest()
|
726
|
+
|
727
|
+
|
533
728
|
class SloGetContext(WSGIContext):
|
534
729
|
|
535
730
|
max_slo_recursion_depth = 10
|
@@ -537,6 +732,8 @@ class SloGetContext(WSGIContext):
|
|
537
732
|
def __init__(self, slo):
|
538
733
|
self.slo = slo
|
539
734
|
super(SloGetContext, self).__init__(slo.app)
|
735
|
+
# we'll know more after we look at the response metadata
|
736
|
+
self.segment_listing_needed = False
|
540
737
|
|
541
738
|
def _fetch_sub_slo_segments(self, req, version, acc, con, obj):
|
542
739
|
"""
|
@@ -557,6 +754,9 @@ class SloGetContext(WSGIContext):
|
|
557
754
|
method='GET',
|
558
755
|
headers={'x-auth-token': req.headers.get('x-auth-token')},
|
559
756
|
agent='%(orig)s SLO MultipartGET', swift_source='SLO')
|
757
|
+
params_copy = dict(req.params)
|
758
|
+
params_copy.pop('part-number', None)
|
759
|
+
sub_req.params = params_copy
|
560
760
|
sub_resp = sub_req.get_response(self.slo.app)
|
561
761
|
|
562
762
|
if not sub_resp.is_success:
|
@@ -571,9 +771,8 @@ class SloGetContext(WSGIContext):
|
|
571
771
|
body if len(body) <= 60 else body[:57] + '...'))
|
572
772
|
|
573
773
|
try:
|
574
|
-
|
575
|
-
|
576
|
-
except ValueError as err:
|
774
|
+
return self._parse_segments(sub_resp.app_iter)
|
775
|
+
except HTTPException as err:
|
577
776
|
raise ListingIterError(
|
578
777
|
'while fetching %s, JSON-decoding of submanifest %s '
|
579
778
|
'failed with %s' % (req.path, sub_req.path, err))
|
@@ -584,32 +783,8 @@ class SloGetContext(WSGIContext):
|
|
584
783
|
conobj=seg_dict['name'].lstrip('/')
|
585
784
|
)
|
586
785
|
|
587
|
-
def _segment_length(self, seg_dict):
|
588
|
-
"""
|
589
|
-
Returns the number of bytes that will be fetched from the specified
|
590
|
-
segment on a plain GET request for this SLO manifest.
|
591
|
-
"""
|
592
|
-
if 'raw_data' in seg_dict:
|
593
|
-
return len(seg_dict['raw_data'])
|
594
|
-
|
595
|
-
seg_range = seg_dict.get('range')
|
596
|
-
if seg_range is not None:
|
597
|
-
# The range is of the form N-M, where N and M are both positive
|
598
|
-
# decimal integers. We know this because this middleware is the
|
599
|
-
# only thing that creates the SLO manifests stored in the
|
600
|
-
# cluster.
|
601
|
-
range_start, range_end = [int(x) for x in seg_range.split('-')]
|
602
|
-
return (range_end - range_start) + 1
|
603
|
-
else:
|
604
|
-
return int(seg_dict['bytes'])
|
605
|
-
|
606
786
|
def _segment_listing_iterator(self, req, version, account, segments,
|
607
787
|
byteranges):
|
608
|
-
for seg_dict in segments:
|
609
|
-
if config_true_value(seg_dict.get('sub_slo')):
|
610
|
-
override_bytes_from_content_type(seg_dict,
|
611
|
-
logger=self.slo.logger)
|
612
|
-
|
613
788
|
# We handle the range stuff here so that we can be smart about
|
614
789
|
# skipping unused submanifests. For example, if our first segment is a
|
615
790
|
# submanifest referencing 50 MiB total, but start_byte falls in
|
@@ -617,9 +792,6 @@ class SloGetContext(WSGIContext):
|
|
617
792
|
#
|
618
793
|
# If we were to make SegmentedIterable handle all the range
|
619
794
|
# calculations, we would be unable to make this optimization.
|
620
|
-
total_length = sum(self._segment_length(seg) for seg in segments)
|
621
|
-
if not byteranges:
|
622
|
-
byteranges = [(0, total_length - 1)]
|
623
795
|
|
624
796
|
# Cache segments from sub-SLOs in case more than one byterange
|
625
797
|
# includes data from a particular sub-SLO. We only cache a few sets
|
@@ -646,12 +818,26 @@ class SloGetContext(WSGIContext):
|
|
646
818
|
first_byte, last_byte,
|
647
819
|
cached_fetch_sub_slo_segments,
|
648
820
|
recursion_depth=1):
|
821
|
+
"""
|
822
|
+
Iterable that generates a filtered and annotated stream of segment
|
823
|
+
dicts describing the sub-segment ranges that would be used by the
|
824
|
+
SegmentedIterable to construct the bytes for a ranged response.
|
825
|
+
|
826
|
+
:param req: original request object
|
827
|
+
:param version: version
|
828
|
+
:param account: account
|
829
|
+
:param segments: segments dictionary
|
830
|
+
:param first_byte: offset into the large object for the first byte
|
831
|
+
that is returned to the client
|
832
|
+
:param last_byte: offset into the large object for the last byte
|
833
|
+
that is returned to the client
|
834
|
+
:param cached_fetch_sub_slo_segments: LRU cache used for fetching
|
835
|
+
sub-segments
|
836
|
+
:param recursion_depth: max number of recursive sub_slo calls
|
837
|
+
"""
|
649
838
|
last_sub_path = None
|
650
839
|
for seg_dict in segments:
|
651
|
-
|
652
|
-
seg_dict['raw_data'] = strict_b64decode(seg_dict.pop('data'))
|
653
|
-
|
654
|
-
seg_length = self._segment_length(seg_dict)
|
840
|
+
seg_length = seg_dict['segment_length']
|
655
841
|
if first_byte >= seg_length:
|
656
842
|
# don't need any bytes from this segment
|
657
843
|
first_byte -= seg_length
|
@@ -718,50 +904,219 @@ class SloGetContext(WSGIContext):
|
|
718
904
|
first_byte -= seg_length
|
719
905
|
last_byte -= seg_length
|
720
906
|
|
721
|
-
def
|
907
|
+
def _is_body_complete(self):
|
908
|
+
content_range = ''
|
909
|
+
for header, value in self._response_headers:
|
910
|
+
if header.lower() == 'content-range':
|
911
|
+
content_range = value
|
912
|
+
break
|
913
|
+
# e.g. Content-Range: bytes 0-14289/14290
|
914
|
+
match = re.match(r'bytes (\d+)-(\d+)/(\d+)$', content_range)
|
915
|
+
if not match:
|
916
|
+
# Malformed or missing, so we don't know what we got.
|
917
|
+
return False
|
918
|
+
first_byte, last_byte, length = [int(x) for x in match.groups()]
|
919
|
+
# If and only if we actually got back the full manifest body, then
|
920
|
+
# we can avoid re-fetching the object.
|
921
|
+
return first_byte == 0 and last_byte == length - 1
|
922
|
+
|
923
|
+
def _need_to_refetch_manifest(self, req, resp_attrs, is_part_num_request):
|
722
924
|
"""
|
723
|
-
|
724
|
-
|
725
|
-
doesn't, we need to make a second request to actually get the whole
|
726
|
-
thing.
|
925
|
+
Check if the segments will be needed to service the request and update
|
926
|
+
the segment_listing_needed attribute.
|
727
927
|
|
728
|
-
|
928
|
+
:return: boolean indicating if we need to refetch, only if the segments
|
929
|
+
ARE needed we MAY need to refetch them!
|
729
930
|
"""
|
730
931
|
if req.method == 'HEAD':
|
731
|
-
#
|
732
|
-
#
|
733
|
-
|
932
|
+
# There may be some cases in the future where a HEAD resp on even a
|
933
|
+
# modern manifest should refetch, e.g. lp bug #2029174
|
934
|
+
self.segment_listing_needed = (resp_attrs.is_legacy or
|
935
|
+
is_part_num_request)
|
936
|
+
# it will always be the case that a HEAD must re-fetch iff
|
937
|
+
# segment_listing_needed
|
938
|
+
return self.segment_listing_needed
|
939
|
+
|
940
|
+
last_resp_status_int = self._get_status_int()
|
941
|
+
# These are based on etag (or last-modified), but the SLO's etag is
|
942
|
+
# almost certainly not the manifest object's etag. Still, it's highly
|
943
|
+
# likely that the submitted If-None-Match won't match the manifest
|
944
|
+
# object's etag, so we can avoid re-fetching the manifest if we got a
|
945
|
+
# successful response.
|
946
|
+
if last_resp_status_int in (412, 304):
|
947
|
+
# a conditional response from a modern manifest would have an
|
948
|
+
# accurate SLO etag, AND comparison with the etag-is-at header, but
|
949
|
+
# for legacy manifests responses (who always need to calculate the
|
950
|
+
# correct etag, even for if-[un]modified-since errors) we can't say
|
951
|
+
# what the etag is or if it matches unless we calculate it from
|
952
|
+
# segments - so we always need them
|
953
|
+
self.segment_listing_needed = resp_attrs.is_legacy
|
954
|
+
# if we need them; we can't get them from the error
|
955
|
+
return self.segment_listing_needed
|
956
|
+
|
957
|
+
# This is GET request for an SLO object, if we're going to return a
|
958
|
+
# successful response we're going to need the segments, but this
|
959
|
+
# resp_iter may not contain the entire SLO manifest.
|
960
|
+
self.segment_listing_needed = True
|
961
|
+
|
962
|
+
# modern swift object-servers should ignore Range headers on manifests,
|
963
|
+
# but during upgrade if we get a range response we'll probably have to
|
964
|
+
# refetch
|
965
|
+
if last_resp_status_int == 416:
|
966
|
+
# if the range wasn't satisfiable we need to refetch
|
734
967
|
return True
|
968
|
+
elif last_resp_status_int == 206:
|
969
|
+
# a partial response might included the whole content-range?!
|
970
|
+
return not self._is_body_complete()
|
971
|
+
else:
|
972
|
+
# a good number of error responses would have returned earlier for
|
973
|
+
# lacking is_slo sys-meta, at this point we've filtered all the
|
974
|
+
# other response codes, so this is a prefectly normal 200 response,
|
975
|
+
# no need to refetch
|
976
|
+
return False
|
977
|
+
|
978
|
+
def _refetch_manifest(self, req, resp_iter, orig_resp_attrs):
|
979
|
+
req.environ['swift.non_client_disconnect'] = True
|
980
|
+
friendly_close(resp_iter)
|
981
|
+
del req.environ['swift.non_client_disconnect']
|
982
|
+
|
983
|
+
get_req = make_subrequest(
|
984
|
+
req.environ, method='GET',
|
985
|
+
headers={'x-auth-token': req.headers.get('x-auth-token')},
|
986
|
+
agent='%(orig)s SLO MultipartGET', swift_source='SLO')
|
987
|
+
resp_iter = self._app_call(get_req.environ)
|
988
|
+
new_resp_attrs = RespAttrs.from_headers(self._response_headers)
|
989
|
+
if new_resp_attrs.timestamp < orig_resp_attrs.timestamp and \
|
990
|
+
not new_resp_attrs.is_slo:
|
991
|
+
# Our *orig_resp_attrs* saw *newer* data that indicated it was an
|
992
|
+
# SLO, but on refetch it's an older object or error; 503 seems
|
993
|
+
# reasonable?
|
994
|
+
friendly_close(resp_iter)
|
995
|
+
raise HTTPServiceUnavailable(request=req)
|
996
|
+
# else, the caller will know how to return this response
|
997
|
+
return new_resp_attrs, resp_iter
|
998
|
+
|
999
|
+
def _parse_segments(self, resp_iter):
|
1000
|
+
"""
|
1001
|
+
Read the manifest body and parse segments.
|
735
1002
|
|
736
|
-
|
1003
|
+
:returns: segments
|
1004
|
+
:raises: HTTPServerError
|
1005
|
+
"""
|
1006
|
+
segments = self._get_manifest_read(resp_iter)
|
1007
|
+
_annotate_segments(segments, logger=self.slo.logger)
|
1008
|
+
return segments
|
737
1009
|
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
1010
|
+
def _return_manifest_response(self, req, start_response, resp_iter,
|
1011
|
+
is_format_raw):
|
1012
|
+
if is_format_raw:
|
1013
|
+
json_data = self.convert_segment_listing(resp_iter)
|
1014
|
+
# we've created a new response body
|
1015
|
+
resp_iter = [json_data]
|
1016
|
+
replace_headers = {
|
1017
|
+
# Note that we have to return the large object's content-type
|
1018
|
+
# (not application/json) so it's like what the client sent on
|
1019
|
+
# PUT. Otherwise, server-side copy won't work.
|
1020
|
+
'Content-Length': len(json_data),
|
1021
|
+
'Etag': md5(json_data, usedforsecurity=False).hexdigest(),
|
1022
|
+
}
|
1023
|
+
else:
|
1024
|
+
# we're going to return the manifest resp_iter as-is
|
1025
|
+
replace_headers = {
|
1026
|
+
'Content-Type': 'application/json; charset=utf-8',
|
1027
|
+
}
|
1028
|
+
return self._return_response(req, start_response, resp_iter,
|
1029
|
+
replace_headers)
|
1030
|
+
|
1031
|
+
def _return_slo_response(self, req, start_response, resp_iter, resp_attrs):
|
1032
|
+
headers = {
|
1033
|
+
'Etag': '"%s"' % resp_attrs.slo_etag,
|
1034
|
+
'X-Manifest-Etag': resp_attrs.json_md5,
|
1035
|
+
# swob will fix this for a GET with Range
|
1036
|
+
'Content-Length': str(resp_attrs.slo_size),
|
1037
|
+
# ignore bogus content-range, make swob figure it out
|
1038
|
+
'Content-Range': None,
|
1039
|
+
}
|
1040
|
+
if self.segment_listing_needed:
|
1041
|
+
# consume existing resp_iter; we'll create a new one
|
1042
|
+
segments = self._parse_segments(resp_iter)
|
1043
|
+
resp_attrs.update_from_segments(segments)
|
1044
|
+
headers['Etag'] = '"%s"' % resp_attrs.slo_etag
|
1045
|
+
headers['Content-Length'] = str(resp_attrs.slo_size)
|
1046
|
+
part_num = get_valid_part_num(req)
|
1047
|
+
if part_num:
|
1048
|
+
headers['X-Parts-Count'] = len(segments)
|
1049
|
+
|
1050
|
+
if part_num and part_num > len(segments):
|
1051
|
+
if req.method == 'HEAD':
|
1052
|
+
resp_iter = []
|
1053
|
+
headers['Content-Length'] = '0'
|
1054
|
+
else:
|
1055
|
+
body = b'The requested part number is not satisfiable'
|
1056
|
+
resp_iter = [body]
|
1057
|
+
headers['Content-Length'] = len(body)
|
1058
|
+
headers['Content-Range'] = 'bytes */%d' % resp_attrs.slo_size
|
1059
|
+
self._response_status = '416 Requested Range Not Satisfiable'
|
1060
|
+
elif part_num and req.method == 'HEAD':
|
1061
|
+
resp_iter = []
|
1062
|
+
headers['Content-Length'] = \
|
1063
|
+
segments[part_num - 1].get('segment_length')
|
1064
|
+
start, end = calculate_byterange_for_part_num(
|
1065
|
+
req, segments, part_num)
|
1066
|
+
headers['Content-Range'] = \
|
1067
|
+
'bytes {}-{}/{}'.format(start, end,
|
1068
|
+
resp_attrs.slo_size)
|
1069
|
+
# The RFC specifies 206 in the context of Range requests, and
|
1070
|
+
# Range headers MUST be ignored for HEADs [1], so a HEAD will
|
1071
|
+
# not normally return a 206. However, a part-number HEAD
|
1072
|
+
# returns Content-Length equal to the part size, rather than
|
1073
|
+
# the whole object size, so in this case we do return 206.
|
1074
|
+
# [1] https://www.rfc-editor.org/rfc/rfc9110#name-range
|
1075
|
+
self._response_status = '206 Partial Content'
|
1076
|
+
elif req.method == 'HEAD':
|
1077
|
+
resp_iter = []
|
1078
|
+
else:
|
1079
|
+
byteranges = calculate_byteranges(
|
1080
|
+
req, segments, resp_attrs, part_num)
|
1081
|
+
resp_iter = self._build_resp_iter(req, segments, byteranges)
|
1082
|
+
return self._return_response(req, start_response, resp_iter,
|
1083
|
+
replace_headers=headers)
|
1084
|
+
|
1085
|
+
def _return_response(self, req, start_response, resp_iter,
|
1086
|
+
replace_headers):
|
1087
|
+
if req.method == 'HEAD' or self._get_status_int() in (412, 304):
|
1088
|
+
# we should drain HEAD and unmet condition responses since they
|
1089
|
+
# don't have bodies
|
1090
|
+
friendly_close(resp_iter)
|
1091
|
+
resp_iter = b''
|
1092
|
+
resp_headers = HeaderKeyDict(self._response_headers, **replace_headers)
|
1093
|
+
resp = Response(
|
1094
|
+
status=self._response_status,
|
1095
|
+
headers=resp_headers,
|
1096
|
+
app_iter=resp_iter,
|
1097
|
+
request=req,
|
1098
|
+
conditional_response=True,
|
1099
|
+
conditional_etag=resolve_etag_is_at_header(req, resp_headers))
|
1100
|
+
return resp(req.environ, start_response)
|
746
1101
|
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
return
|
1102
|
+
def _return_non_slo_response(self, req, start_response, resp_iter):
|
1103
|
+
# our "pass-through" response may have been from a manifest refetch w/o
|
1104
|
+
# range/conditional headers that turned out to be a real object, and
|
1105
|
+
# now we want out. But if the original client request included Range
|
1106
|
+
# or Conditional headers we can trust swob to do the right conversion
|
1107
|
+
# back into a 206/416/304/412 (as long as the response we have is a
|
1108
|
+
# normal successful response and we respect any forwarding middleware's
|
1109
|
+
# etag-is-at header that we stripped off for the refetch!)
|
1110
|
+
resp = Response(
|
1111
|
+
status=self._response_status,
|
1112
|
+
headers=self._response_headers,
|
1113
|
+
app_iter=resp_iter,
|
1114
|
+
request=req,
|
1115
|
+
conditional_response=self._get_status_int() == 200,
|
1116
|
+
conditional_etag=resolve_etag_is_at_header(
|
1117
|
+
req, self._response_headers)
|
1118
|
+
)
|
1119
|
+
return resp(req.environ, start_response)
|
765
1120
|
|
766
1121
|
def handle_slo_get_or_head(self, req, start_response):
|
767
1122
|
"""
|
@@ -774,137 +1129,72 @@ class SloGetContext(WSGIContext):
|
|
774
1129
|
large object manifest.
|
775
1130
|
:param start_response: WSGI start_response callable
|
776
1131
|
"""
|
777
|
-
|
1132
|
+
is_manifest_get = get_param(req, 'multipart-manifest') == 'get'
|
1133
|
+
is_format_raw = is_manifest_get and get_param(req, 'format') == 'raw'
|
1134
|
+
|
1135
|
+
if not is_manifest_get:
|
778
1136
|
# If this object is an SLO manifest, we may have saved off the
|
779
1137
|
# large object etag during the original PUT. Send an
|
780
|
-
# X-Backend-Etag-Is-At header so that, if the SLO etag *was*
|
781
|
-
#
|
782
|
-
#
|
1138
|
+
# X-Backend-Etag-Is-At header so that, if the SLO etag *was* saved,
|
1139
|
+
# we can trust the object-server to respond appropriately to
|
1140
|
+
# If-Match/If-None-Match requests.
|
783
1141
|
update_etag_is_at_header(req, SYSMETA_SLO_ETAG)
|
784
1142
|
# Tell the object server that if it's a manifest,
|
785
1143
|
# we want the whole thing
|
786
1144
|
update_ignore_range_header(req, 'X-Static-Large-Object')
|
787
|
-
resp_iter = self._app_call(req.environ)
|
788
|
-
|
789
|
-
# make sure this response is for a static large object manifest
|
790
|
-
slo_marker = slo_etag = slo_size = slo_timestamp = None
|
791
|
-
for header, value in self._response_headers:
|
792
|
-
header = header.lower()
|
793
|
-
if header == SYSMETA_SLO_ETAG:
|
794
|
-
slo_etag = value
|
795
|
-
elif header == SYSMETA_SLO_SIZE:
|
796
|
-
slo_size = value
|
797
|
-
elif (header == 'x-static-large-object' and
|
798
|
-
config_true_value(value)):
|
799
|
-
slo_marker = value
|
800
|
-
elif header == 'x-backend-timestamp':
|
801
|
-
slo_timestamp = value
|
802
|
-
|
803
|
-
if slo_marker and slo_etag and slo_size and slo_timestamp:
|
804
|
-
break
|
805
|
-
|
806
|
-
if not slo_marker:
|
807
|
-
# Not a static large object manifest. Just pass it through.
|
808
|
-
start_response(self._response_status,
|
809
|
-
self._response_headers,
|
810
|
-
self._response_exc_info)
|
811
|
-
return resp_iter
|
812
|
-
|
813
|
-
# Handle pass-through request for the manifest itself
|
814
|
-
if req.params.get('multipart-manifest') == 'get':
|
815
|
-
if req.params.get('format') == 'raw':
|
816
|
-
resp_iter = self.convert_segment_listing(
|
817
|
-
self._response_headers, resp_iter)
|
818
|
-
else:
|
819
|
-
new_headers = []
|
820
|
-
for header, value in self._response_headers:
|
821
|
-
if header.lower() == 'content-type':
|
822
|
-
new_headers.append(('Content-Type',
|
823
|
-
'application/json; charset=utf-8'))
|
824
|
-
else:
|
825
|
-
new_headers.append((header, value))
|
826
|
-
self._response_headers = new_headers
|
827
|
-
start_response(self._response_status,
|
828
|
-
self._response_headers,
|
829
|
-
self._response_exc_info)
|
830
|
-
return resp_iter
|
831
|
-
|
832
|
-
is_conditional = self._response_status.startswith(('304', '412')) and (
|
833
|
-
req.if_match or req.if_none_match)
|
834
|
-
if slo_etag and slo_size and (
|
835
|
-
req.method == 'HEAD' or is_conditional):
|
836
|
-
# Since we have length and etag, we can respond immediately
|
837
|
-
resp = Response(
|
838
|
-
status=self._response_status,
|
839
|
-
headers=self._response_headers,
|
840
|
-
app_iter=resp_iter,
|
841
|
-
request=req,
|
842
|
-
conditional_etag=resolve_etag_is_at_header(
|
843
|
-
req, self._response_headers),
|
844
|
-
conditional_response=True)
|
845
|
-
resp.headers.update({
|
846
|
-
'Etag': '"%s"' % slo_etag,
|
847
|
-
'X-Manifest-Etag': self._response_header_value('etag'),
|
848
|
-
'Content-Length': slo_size,
|
849
|
-
})
|
850
|
-
return resp(req.environ, start_response)
|
851
1145
|
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
response = self.get_or_head_response(
|
898
|
-
req, resp_headers, resp_iter)
|
899
|
-
return response(req.environ, start_response)
|
900
|
-
|
901
|
-
def convert_segment_listing(self, resp_headers, resp_iter):
|
1146
|
+
# process original request
|
1147
|
+
orig_path_info = req.path_info
|
1148
|
+
resp_iter = self._app_call(req.environ)
|
1149
|
+
resp_attrs = RespAttrs.from_headers(self._response_headers)
|
1150
|
+
if resp_attrs.is_slo and not is_manifest_get:
|
1151
|
+
try:
|
1152
|
+
# only validate part-number if the request is to an SLO
|
1153
|
+
part_num = get_valid_part_num(req)
|
1154
|
+
except HTTPException:
|
1155
|
+
friendly_close(resp_iter)
|
1156
|
+
raise
|
1157
|
+
# the next two calls hide a couple side effects, sorry:
|
1158
|
+
#
|
1159
|
+
# 1) regardless of the return value the "need_to_refetch" check
|
1160
|
+
# *may* also set self.segment_listing_needed = True (it's
|
1161
|
+
# commented to help you wrap your head around that one,
|
1162
|
+
# good luck)
|
1163
|
+
# 2) if we refetch, we overwrite the current resp_iter and
|
1164
|
+
# resp_attrs variables, partly because we *might* get back a NOT
|
1165
|
+
# resp_attrs.is_slo response (even if we had one to start), but
|
1166
|
+
# hopefully they're just the manifest resp we needed to refetch!
|
1167
|
+
if self._need_to_refetch_manifest(req, resp_attrs, part_num):
|
1168
|
+
# reset path in case it was modified during original request
|
1169
|
+
# (e.g. object versioning might re-write the path)
|
1170
|
+
req.path_info = orig_path_info
|
1171
|
+
resp_attrs, resp_iter = self._refetch_manifest(
|
1172
|
+
req, resp_iter, resp_attrs)
|
1173
|
+
|
1174
|
+
if not resp_attrs.is_slo:
|
1175
|
+
# even if the original resp_attrs may have been SLO we may have
|
1176
|
+
# refetched, this also handles the server error case
|
1177
|
+
return self._return_non_slo_response(
|
1178
|
+
req, start_response, resp_iter)
|
1179
|
+
|
1180
|
+
if is_manifest_get:
|
1181
|
+
# manifest pass through doesn't require resp_attrs
|
1182
|
+
return self._return_manifest_response(req, start_response,
|
1183
|
+
resp_iter, is_format_raw)
|
1184
|
+
|
1185
|
+
# this a GET/HEAD response for the SLO object (not the manifest)
|
1186
|
+
return self._return_slo_response(req, start_response, resp_iter,
|
1187
|
+
resp_attrs)
|
1188
|
+
|
1189
|
+
def convert_segment_listing(self, resp_iter):
|
902
1190
|
"""
|
903
1191
|
Converts the manifest data to match with the format
|
904
1192
|
that was put in through ?multipart-manifest=put
|
905
1193
|
|
906
|
-
:param resp_headers: response headers
|
907
1194
|
:param resp_iter: a response iterable
|
1195
|
+
|
1196
|
+
:raises HTTPServerError:
|
1197
|
+
:returns: the json-serialized raw format (as bytes)
|
908
1198
|
"""
|
909
1199
|
segments = self._get_manifest_read(resp_iter)
|
910
1200
|
|
@@ -921,109 +1211,29 @@ class SloGetContext(WSGIContext):
|
|
921
1211
|
json_data = json.dumps(segments, sort_keys=True) # convert to string
|
922
1212
|
if six.PY3:
|
923
1213
|
json_data = json_data.encode('utf-8')
|
924
|
-
|
925
|
-
new_headers = []
|
926
|
-
for header, value in resp_headers:
|
927
|
-
if header.lower() == 'content-length':
|
928
|
-
new_headers.append(('Content-Length', len(json_data)))
|
929
|
-
elif header.lower() == 'etag':
|
930
|
-
new_headers.append(
|
931
|
-
('Etag', md5(json_data, usedforsecurity=False)
|
932
|
-
.hexdigest()))
|
933
|
-
else:
|
934
|
-
new_headers.append((header, value))
|
935
|
-
self._response_headers = new_headers
|
936
|
-
|
937
|
-
return [json_data]
|
1214
|
+
return json_data
|
938
1215
|
|
939
1216
|
def _get_manifest_read(self, resp_iter):
|
940
1217
|
with closing_if_possible(resp_iter):
|
941
1218
|
resp_body = b''.join(resp_iter)
|
942
1219
|
try:
|
943
1220
|
segments = json.loads(resp_body)
|
944
|
-
except ValueError:
|
945
|
-
|
946
|
-
|
1221
|
+
except ValueError as e:
|
1222
|
+
msg = 'Unable to load SLO manifest'
|
1223
|
+
self.slo.logger.error('%s: %s', msg, e)
|
1224
|
+
raise HTTPServerError(msg)
|
947
1225
|
return segments
|
948
1226
|
|
949
|
-
def
|
950
|
-
|
951
|
-
|
952
|
-
content_length = None
|
953
|
-
response_headers = []
|
954
|
-
for header, value in resp_headers:
|
955
|
-
lheader = header.lower()
|
956
|
-
if lheader == 'etag':
|
957
|
-
response_headers.append(('X-Manifest-Etag', value))
|
958
|
-
elif lheader != 'content-length':
|
959
|
-
response_headers.append((header, value))
|
960
|
-
|
961
|
-
if lheader == SYSMETA_SLO_ETAG:
|
962
|
-
slo_etag = value
|
963
|
-
elif lheader == SYSMETA_SLO_SIZE:
|
964
|
-
# it's from sysmeta, so we don't worry about non-integer
|
965
|
-
# values here
|
966
|
-
content_length = int(value)
|
967
|
-
|
968
|
-
# Prep to calculate content_length & etag if necessary
|
969
|
-
if slo_etag is None:
|
970
|
-
calculated_etag = md5(usedforsecurity=False)
|
971
|
-
if content_length is None:
|
972
|
-
calculated_content_length = 0
|
973
|
-
|
974
|
-
for seg_dict in segments:
|
975
|
-
# Decode any inlined data; it's important that we do this *before*
|
976
|
-
# calculating the segment length and etag
|
977
|
-
if 'data' in seg_dict:
|
978
|
-
seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
|
979
|
-
|
980
|
-
if slo_etag is None:
|
981
|
-
if 'raw_data' in seg_dict:
|
982
|
-
r = md5(seg_dict['raw_data'],
|
983
|
-
usedforsecurity=False).hexdigest()
|
984
|
-
elif seg_dict.get('range'):
|
985
|
-
r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
|
986
|
-
else:
|
987
|
-
r = seg_dict['hash']
|
988
|
-
calculated_etag.update(r.encode('ascii'))
|
989
|
-
|
990
|
-
if content_length is None:
|
991
|
-
if config_true_value(seg_dict.get('sub_slo')):
|
992
|
-
override_bytes_from_content_type(
|
993
|
-
seg_dict, logger=self.slo.logger)
|
994
|
-
calculated_content_length += self._segment_length(seg_dict)
|
995
|
-
|
996
|
-
if slo_etag is None:
|
997
|
-
slo_etag = calculated_etag.hexdigest()
|
998
|
-
if content_length is None:
|
999
|
-
content_length = calculated_content_length
|
1000
|
-
|
1001
|
-
response_headers.append(('Content-Length', str(content_length)))
|
1002
|
-
response_headers.append(('Etag', '"%s"' % slo_etag))
|
1227
|
+
def _build_resp_iter(self, req, segments, byteranges):
|
1228
|
+
"""
|
1229
|
+
Build a response iterable for a GET request.
|
1003
1230
|
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
return self._manifest_get_response(
|
1008
|
-
req, content_length, response_headers, segments)
|
1009
|
-
|
1010
|
-
def _manifest_head_response(self, req, response_headers):
|
1011
|
-
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
1012
|
-
return HTTPOk(request=req, headers=response_headers, body=b'',
|
1013
|
-
conditional_etag=conditional_etag,
|
1014
|
-
conditional_response=True)
|
1015
|
-
|
1016
|
-
def _manifest_get_response(self, req, content_length, response_headers,
|
1017
|
-
segments):
|
1018
|
-
if req.range:
|
1019
|
-
byteranges = [
|
1020
|
-
# For some reason, swob.Range.ranges_for_length adds 1 to the
|
1021
|
-
# last byte's position.
|
1022
|
-
(start, end - 1) for start, end
|
1023
|
-
in req.range.ranges_for_length(content_length)]
|
1024
|
-
else:
|
1025
|
-
byteranges = []
|
1231
|
+
:param req: the request object
|
1232
|
+
:param segments: the list of seg_dicts
|
1233
|
+
:param byteranges: a list of tuples representing byteranges
|
1026
1234
|
|
1235
|
+
:returns: a segmented iterable
|
1236
|
+
"""
|
1027
1237
|
ver, account, _junk = req.split_path(3, 3, rest_with_last=True)
|
1028
1238
|
account = wsgi_to_str(account)
|
1029
1239
|
plain_listing_iter = self._segment_listing_iterator(
|
@@ -1067,15 +1277,8 @@ class SloGetContext(WSGIContext):
|
|
1067
1277
|
# their Etag/Content Length no longer match the connection
|
1068
1278
|
# will drop. In this case a 409 Conflict will be logged in
|
1069
1279
|
# the proxy logs and the user will receive incomplete results.
|
1070
|
-
|
1071
|
-
|
1072
|
-
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
1073
|
-
response = Response(request=req, content_length=content_length,
|
1074
|
-
headers=response_headers,
|
1075
|
-
conditional_response=True,
|
1076
|
-
conditional_etag=conditional_etag,
|
1077
|
-
app_iter=segmented_iter)
|
1078
|
-
return response
|
1280
|
+
raise HTTPConflict(request=req)
|
1281
|
+
return segmented_iter
|
1079
1282
|
|
1080
1283
|
|
1081
1284
|
class StaticLargeObject(object):
|
@@ -1128,12 +1331,7 @@ class StaticLargeObject(object):
|
|
1128
1331
|
delete_concurrency=delete_concurrency,
|
1129
1332
|
logger=self.logger)
|
1130
1333
|
|
1131
|
-
|
1132
|
-
if conf.get('auto_create_account_prefix'):
|
1133
|
-
# proxy app will log about how this should get moved to swift.conf
|
1134
|
-
prefix = conf['auto_create_account_prefix']
|
1135
|
-
else:
|
1136
|
-
prefix = AUTO_CREATE_ACCOUNT_PREFIX
|
1334
|
+
prefix = AUTO_CREATE_ACCOUNT_PREFIX
|
1137
1335
|
self.expiring_objects_account = prefix + (
|
1138
1336
|
conf.get('expiring_objects_account_name') or 'expiring_objects')
|
1139
1337
|
self.expiring_objects_container_divisor = int(
|
@@ -1505,7 +1703,7 @@ class StaticLargeObject(object):
|
|
1505
1703
|
vrs, account, _junk = req.split_path(2, 3, True)
|
1506
1704
|
new_env = req.environ.copy()
|
1507
1705
|
new_env['REQUEST_METHOD'] = 'GET'
|
1508
|
-
del
|
1706
|
+
del new_env['wsgi.input']
|
1509
1707
|
new_env['QUERY_STRING'] = 'multipart-manifest=get'
|
1510
1708
|
if 'version-id' in req.params:
|
1511
1709
|
new_env['QUERY_STRING'] += \
|
@@ -1524,7 +1722,7 @@ class StaticLargeObject(object):
|
|
1524
1722
|
'/%s/%s/%s' % (vrs, account, str_to_wsgi(obj_name.lstrip('/')))
|
1525
1723
|
)
|
1526
1724
|
# Just request the last byte of non-SLO objects so we don't waste
|
1527
|
-
# a
|
1725
|
+
# a resources in friendly_close() below
|
1528
1726
|
manifest_req = Request.blank('', new_env, range='bytes=-1')
|
1529
1727
|
update_ignore_range_header(manifest_req, 'X-Static-Large-Object')
|
1530
1728
|
resp = manifest_req.get_response(self.app)
|
@@ -1543,7 +1741,7 @@ class StaticLargeObject(object):
|
|
1543
1741
|
raise HTTPServerError('Unable to load SLO manifest')
|
1544
1742
|
else:
|
1545
1743
|
# Drain and close GET request (prevents socket leaks)
|
1546
|
-
|
1744
|
+
friendly_close(resp)
|
1547
1745
|
raise HTTPBadRequest('Not an SLO manifest')
|
1548
1746
|
elif resp.status_int == HTTP_NOT_FOUND:
|
1549
1747
|
raise HTTPNotFound('SLO manifest not found')
|
@@ -1624,7 +1822,7 @@ class StaticLargeObject(object):
|
|
1624
1822
|
resp.status, resp.body)
|
1625
1823
|
return HTTPServiceUnavailable()
|
1626
1824
|
# consume the response (should be short)
|
1627
|
-
|
1825
|
+
friendly_close(resp)
|
1628
1826
|
|
1629
1827
|
# Finally, delete the manifest
|
1630
1828
|
return self.app
|