swift 2.23.3__py3-none-any.whl → 2.35.0__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/__init__.py +29 -50
- swift/account/auditor.py +21 -118
- swift/account/backend.py +33 -28
- swift/account/reaper.py +37 -28
- swift/account/replicator.py +22 -0
- swift/account/server.py +60 -26
- swift/account/utils.py +28 -11
- swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
- swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
- swift/cli/container_deleter.py +5 -11
- swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
- swift/cli/dispersion_report.py +10 -9
- swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
- swift/cli/form_signature.py +3 -7
- swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
- swift/cli/info.py +154 -14
- swift/cli/manage_shard_ranges.py +705 -37
- swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
- swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
- swift/cli/recon.py +196 -67
- swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
- swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +807 -126
- swift/cli/reload.py +135 -0
- swift/cli/ringbuilder.py +217 -20
- swift/cli/ringcomposer.py +0 -1
- swift/cli/shard-info.py +4 -3
- swift/common/base_storage_server.py +9 -20
- swift/common/bufferedhttp.py +48 -74
- swift/common/constraints.py +20 -15
- swift/common/container_sync_realms.py +9 -11
- swift/common/daemon.py +25 -8
- swift/common/db.py +195 -128
- swift/common/db_auditor.py +168 -0
- swift/common/db_replicator.py +95 -55
- swift/common/digest.py +141 -0
- swift/common/direct_client.py +144 -33
- swift/common/error_limiter.py +93 -0
- swift/common/exceptions.py +25 -1
- swift/common/header_key_dict.py +2 -9
- swift/common/http_protocol.py +373 -0
- swift/common/internal_client.py +129 -59
- swift/common/linkat.py +3 -4
- swift/common/manager.py +284 -67
- swift/common/memcached.py +390 -145
- swift/common/middleware/__init__.py +4 -0
- swift/common/middleware/account_quotas.py +211 -46
- swift/common/middleware/acl.py +3 -8
- swift/common/middleware/backend_ratelimit.py +230 -0
- swift/common/middleware/bulk.py +22 -34
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +6 -11
- swift/common/middleware/container_quotas.py +1 -1
- swift/common/middleware/container_sync.py +39 -17
- swift/common/middleware/copy.py +12 -0
- swift/common/middleware/crossdomain.py +22 -9
- swift/common/middleware/crypto/__init__.py +2 -1
- swift/common/middleware/crypto/crypto_utils.py +11 -15
- swift/common/middleware/crypto/decrypter.py +28 -11
- swift/common/middleware/crypto/encrypter.py +12 -17
- swift/common/middleware/crypto/keymaster.py +8 -15
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/dlo.py +15 -11
- swift/common/middleware/domain_remap.py +5 -4
- swift/common/middleware/etag_quoter.py +128 -0
- swift/common/middleware/formpost.py +73 -70
- swift/common/middleware/gatekeeper.py +8 -1
- swift/common/middleware/keystoneauth.py +33 -3
- swift/common/middleware/list_endpoints.py +4 -4
- swift/common/middleware/listing_formats.py +85 -49
- swift/common/middleware/memcache.py +4 -95
- swift/common/middleware/name_check.py +3 -2
- swift/common/middleware/proxy_logging.py +160 -92
- swift/common/middleware/ratelimit.py +17 -10
- swift/common/middleware/read_only.py +6 -4
- swift/common/middleware/recon.py +59 -22
- swift/common/middleware/s3api/acl_handlers.py +25 -3
- swift/common/middleware/s3api/acl_utils.py +6 -1
- swift/common/middleware/s3api/controllers/__init__.py +6 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/bucket.py +242 -137
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
- swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
- swift/common/middleware/s3api/controllers/obj.py +112 -8
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
- swift/common/middleware/s3api/controllers/tagging.py +57 -0
- swift/common/middleware/s3api/controllers/versioning.py +36 -7
- swift/common/middleware/s3api/etree.py +22 -9
- swift/common/middleware/s3api/exception.py +0 -4
- swift/common/middleware/s3api/s3api.py +113 -41
- swift/common/middleware/s3api/s3request.py +384 -218
- swift/common/middleware/s3api/s3response.py +126 -23
- swift/common/middleware/s3api/s3token.py +16 -17
- swift/common/middleware/s3api/schema/delete.rng +1 -1
- swift/common/middleware/s3api/subresource.py +7 -10
- swift/common/middleware/s3api/utils.py +27 -10
- swift/common/middleware/slo.py +665 -358
- swift/common/middleware/staticweb.py +64 -37
- swift/common/middleware/symlink.py +51 -18
- swift/common/middleware/tempauth.py +76 -58
- swift/common/middleware/tempurl.py +191 -173
- swift/common/middleware/versioned_writes/__init__.py +51 -0
- swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
- swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +18 -19
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +10 -10
- swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
- swift/common/registry.py +147 -0
- swift/common/request_helpers.py +324 -57
- swift/common/ring/builder.py +67 -25
- swift/common/ring/composite_builder.py +1 -1
- swift/common/ring/ring.py +177 -51
- swift/common/ring/utils.py +1 -1
- swift/common/splice.py +10 -6
- swift/common/statsd_client.py +205 -0
- swift/common/storage_policy.py +49 -44
- swift/common/swob.py +86 -102
- swift/common/{utils.py → utils/__init__.py} +2163 -2772
- swift/common/utils/base.py +131 -0
- swift/common/utils/config.py +433 -0
- swift/common/utils/ipaddrs.py +256 -0
- swift/common/utils/libc.py +345 -0
- swift/common/utils/logs.py +859 -0
- swift/common/utils/timestamp.py +412 -0
- swift/common/wsgi.py +553 -535
- swift/container/auditor.py +14 -100
- swift/container/backend.py +490 -231
- swift/container/reconciler.py +126 -37
- swift/container/replicator.py +96 -22
- swift/container/server.py +358 -165
- swift/container/sharder.py +1540 -684
- swift/container/sync.py +94 -88
- swift/container/updater.py +53 -32
- swift/obj/auditor.py +153 -35
- swift/obj/diskfile.py +466 -217
- swift/obj/expirer.py +406 -124
- swift/obj/mem_diskfile.py +7 -4
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +523 -262
- swift/obj/replicator.py +249 -188
- swift/obj/server.py +207 -122
- swift/obj/ssync_receiver.py +145 -85
- swift/obj/ssync_sender.py +113 -54
- swift/obj/updater.py +652 -139
- swift/obj/watchers/__init__.py +0 -0
- swift/obj/watchers/dark_data.py +213 -0
- swift/proxy/controllers/account.py +11 -11
- swift/proxy/controllers/base.py +848 -604
- swift/proxy/controllers/container.py +433 -92
- swift/proxy/controllers/info.py +3 -2
- swift/proxy/controllers/obj.py +1000 -489
- swift/proxy/server.py +185 -112
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
- swift-2.35.0.dist-info/RECORD +201 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
- swift-2.35.0.dist-info/pbr.json +1 -0
- swift/locale/de/LC_MESSAGES/swift.po +0 -1216
- swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
- swift/locale/es/LC_MESSAGES/swift.po +0 -1085
- swift/locale/fr/LC_MESSAGES/swift.po +0 -909
- swift/locale/it/LC_MESSAGES/swift.po +0 -894
- swift/locale/ja/LC_MESSAGES/swift.po +0 -965
- swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
- swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
- swift/locale/ru/LC_MESSAGES/swift.po +0 -891
- swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
- swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
- swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
- swift-2.23.3.data/scripts/swift-account-auditor +0 -23
- swift-2.23.3.data/scripts/swift-account-info +0 -51
- swift-2.23.3.data/scripts/swift-account-reaper +0 -23
- swift-2.23.3.data/scripts/swift-account-replicator +0 -34
- swift-2.23.3.data/scripts/swift-account-server +0 -23
- swift-2.23.3.data/scripts/swift-container-auditor +0 -23
- swift-2.23.3.data/scripts/swift-container-info +0 -55
- swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
- swift-2.23.3.data/scripts/swift-container-replicator +0 -34
- swift-2.23.3.data/scripts/swift-container-sharder +0 -37
- swift-2.23.3.data/scripts/swift-container-sync +0 -23
- swift-2.23.3.data/scripts/swift-container-updater +0 -23
- swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
- swift-2.23.3.data/scripts/swift-form-signature +0 -20
- swift-2.23.3.data/scripts/swift-init +0 -119
- swift-2.23.3.data/scripts/swift-object-auditor +0 -29
- swift-2.23.3.data/scripts/swift-object-expirer +0 -33
- swift-2.23.3.data/scripts/swift-object-info +0 -60
- swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
- swift-2.23.3.data/scripts/swift-object-relinker +0 -41
- swift-2.23.3.data/scripts/swift-object-replicator +0 -37
- swift-2.23.3.data/scripts/swift-object-server +0 -27
- swift-2.23.3.data/scripts/swift-object-updater +0 -23
- swift-2.23.3.data/scripts/swift-proxy-server +0 -23
- swift-2.23.3.data/scripts/swift-recon +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.23.3.data/scripts/swift-ring-composer +0 -22
- swift-2.23.3.dist-info/RECORD +0 -220
- swift-2.23.3.dist-info/pbr.json +0 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
- {swift-2.23.3.dist-info → swift-2.35.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
|
@@ -290,6 +306,16 @@ A ``DELETE`` with a query parameter::
|
|
290
306
|
will delete all the segments referenced in the manifest and then the manifest
|
291
307
|
itself. The failure response will be similar to the bulk delete middleware.
|
292
308
|
|
309
|
+
A ``DELETE`` with the query parameters::
|
310
|
+
|
311
|
+
?multipart-manifest=delete&async=yes
|
312
|
+
|
313
|
+
will schedule all the segments referenced in the manifest to be deleted
|
314
|
+
asynchronously and then delete the manifest itself. Note that segments will
|
315
|
+
continue to appear in listings and be counted for quotas until they are
|
316
|
+
cleaned up by the object-expirer. This option is only available when all
|
317
|
+
segments are in the same container and none of them are nested SLOs.
|
318
|
+
|
293
319
|
------------------------
|
294
320
|
Modifying a Large Object
|
295
321
|
------------------------
|
@@ -313,17 +339,15 @@ metadata which can be used for stats and billing purposes.
|
|
313
339
|
"""
|
314
340
|
|
315
341
|
import base64
|
316
|
-
from cgi import parse_header
|
317
342
|
from collections import defaultdict
|
318
343
|
from datetime import datetime
|
319
344
|
import json
|
320
345
|
import mimetypes
|
321
346
|
import re
|
322
347
|
import time
|
323
|
-
from hashlib import md5
|
324
|
-
|
325
|
-
import six
|
326
348
|
|
349
|
+
from swift.cli.container_deleter import make_delete_jobs
|
350
|
+
from swift.common.header_key_dict import HeaderKeyDict
|
327
351
|
from swift.common.exceptions import ListingIterError, SegmentError
|
328
352
|
from swift.common.middleware.listing_formats import \
|
329
353
|
MAX_CONTAINER_LISTING_CONTENT_LENGTH
|
@@ -331,21 +355,26 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
|
|
331
355
|
HTTPMethodNotAllowed, HTTPRequestEntityTooLarge, HTTPLengthRequired, \
|
332
356
|
HTTPOk, HTTPPreconditionFailed, HTTPException, HTTPNotFound, \
|
333
357
|
HTTPUnauthorized, HTTPConflict, HTTPUnprocessableEntity, \
|
334
|
-
HTTPServiceUnavailable, Response, Range, \
|
335
|
-
RESPONSE_REASONS, str_to_wsgi, wsgi_to_str, wsgi_quote
|
358
|
+
HTTPServiceUnavailable, Response, Range, normalize_etag, \
|
359
|
+
RESPONSE_REASONS, str_to_wsgi, bytes_to_wsgi, wsgi_to_str, wsgi_quote
|
336
360
|
from swift.common.utils import get_logger, config_true_value, \
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
361
|
+
override_bytes_from_content_type, split_path, \
|
362
|
+
RateLimitedIterator, quote, closing_if_possible, \
|
363
|
+
LRUCache, StreamingPile, strict_b64decode, Timestamp, friendly_close, \
|
364
|
+
md5, parse_header
|
365
|
+
from swift.common.registry import register_swift_info
|
341
366
|
from swift.common.request_helpers import SegmentedIterable, \
|
342
367
|
get_sys_meta_prefix, update_etag_is_at_header, resolve_etag_is_at_header, \
|
343
|
-
get_container_update_override_key
|
368
|
+
get_container_update_override_key, update_ignore_range_header, \
|
369
|
+
get_param, get_valid_part_num
|
344
370
|
from swift.common.constraints import check_utf8
|
345
|
-
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
346
|
-
from swift.common.wsgi import WSGIContext, make_subrequest
|
371
|
+
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
372
|
+
from swift.common.wsgi import WSGIContext, make_subrequest, make_env, \
|
373
|
+
make_pre_authed_request
|
347
374
|
from swift.common.middleware.bulk import get_response_body, \
|
348
375
|
ACCEPTABLE_FORMATS, Bulk
|
376
|
+
from swift.obj import expirer
|
377
|
+
from swift.proxy.controllers.base import get_container_info
|
349
378
|
|
350
379
|
|
351
380
|
DEFAULT_RATE_LIMIT_UNDER_SIZE = 1024 ** 2 # 1 MiB
|
@@ -431,12 +460,12 @@ def parse_and_validate_input(req_body, req_path):
|
|
431
460
|
continue
|
432
461
|
|
433
462
|
if segment_type == 'path':
|
434
|
-
if not isinstance(seg_dict['path'],
|
463
|
+
if not isinstance(seg_dict['path'], str):
|
435
464
|
errors.append(b"Index %d: \"path\" must be a string" %
|
436
465
|
seg_index)
|
437
466
|
continue
|
438
467
|
if not (seg_dict.get('etag') is None or
|
439
|
-
isinstance(seg_dict['etag'],
|
468
|
+
isinstance(seg_dict['etag'], str)):
|
440
469
|
errors.append(b'Index %d: "etag" must be a string or null '
|
441
470
|
b'(if provided)' % seg_index)
|
442
471
|
continue
|
@@ -517,6 +546,183 @@ def parse_and_validate_input(req_body, req_path):
|
|
517
546
|
return parsed_data
|
518
547
|
|
519
548
|
|
549
|
+
def _annotate_segments(segments, logger=None):
|
550
|
+
"""
|
551
|
+
Decode any inlined data and update sub_slo segments bytes from content-type
|
552
|
+
when available; then annotate segment dicts in segments list with
|
553
|
+
'segment_length'.
|
554
|
+
|
555
|
+
N.B. raw_data segments don't have a bytes key and range-segments need to
|
556
|
+
calculate their length from their range key but afterwards all segments
|
557
|
+
dicts will have 'segment_length' representing the length of the segment.
|
558
|
+
"""
|
559
|
+
for seg_dict in segments:
|
560
|
+
if 'data' in seg_dict:
|
561
|
+
seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
|
562
|
+
segment_length = len(seg_dict['raw_data'])
|
563
|
+
else:
|
564
|
+
if config_true_value(seg_dict.get('sub_slo')):
|
565
|
+
override_bytes_from_content_type(
|
566
|
+
seg_dict, logger=logger)
|
567
|
+
seg_range = seg_dict.get('range')
|
568
|
+
if seg_range is not None:
|
569
|
+
# The range is of the form N-M, where N and M are both
|
570
|
+
# positive decimal integers. We know this because this
|
571
|
+
# middleware is the only thing that creates the SLO
|
572
|
+
# manifests stored in the cluster.
|
573
|
+
range_start, range_end = [
|
574
|
+
int(x) for x in seg_range.split('-')]
|
575
|
+
segment_length = (range_end - range_start) + 1
|
576
|
+
else:
|
577
|
+
segment_length = int(seg_dict['bytes'])
|
578
|
+
seg_dict['segment_length'] = segment_length
|
579
|
+
|
580
|
+
|
581
|
+
def calculate_byterange_for_part_num(req, segments, part_num):
|
582
|
+
"""
|
583
|
+
Helper function to calculate the byterange for a part_num response.
|
584
|
+
|
585
|
+
N.B. as a side-effect of calculating the single tuple representing the
|
586
|
+
byterange required for a part_num response this function will also mutate
|
587
|
+
the request's Range header so that swob knows to return 206.
|
588
|
+
|
589
|
+
:param req: the request object
|
590
|
+
:param segments: the list of seg_dicts
|
591
|
+
:param part_num: the part number of the object to return
|
592
|
+
|
593
|
+
:returns: a tuple representing the byterange
|
594
|
+
"""
|
595
|
+
start = 0
|
596
|
+
for seg in segments[:part_num - 1]:
|
597
|
+
start += seg['segment_length']
|
598
|
+
last = start + segments[part_num - 1]['segment_length']
|
599
|
+
# We need to mutate the request's Range header so that swob knows to
|
600
|
+
# handle these partial content requests correctly.
|
601
|
+
req.range = "bytes=%d-%d" % (start, last - 1)
|
602
|
+
return start, last - 1
|
603
|
+
|
604
|
+
|
605
|
+
def calculate_byteranges(req, segments, resp_attrs, part_num):
|
606
|
+
"""
|
607
|
+
Calculate the byteranges based on the request, segments, and part number.
|
608
|
+
|
609
|
+
N.B. as a side-effect of calculating the single tuple representing the
|
610
|
+
byterange required for a part_num response this function will also mutate
|
611
|
+
the request's Range header so that swob knows to return 206.
|
612
|
+
|
613
|
+
:param req: the request object
|
614
|
+
:param segments: the list of seg_dicts
|
615
|
+
:param resp_attrs: the slo response attributes
|
616
|
+
:param part_num: the part number of the object to return
|
617
|
+
|
618
|
+
:returns: a list of tuples representing byteranges
|
619
|
+
"""
|
620
|
+
if req.range:
|
621
|
+
byteranges = [
|
622
|
+
# For some reason, swob.Range.ranges_for_length adds 1 to the
|
623
|
+
# last byte's position.
|
624
|
+
(start, end - 1) for start, end
|
625
|
+
in req.range.ranges_for_length(resp_attrs.slo_size)]
|
626
|
+
elif part_num:
|
627
|
+
byteranges = [
|
628
|
+
calculate_byterange_for_part_num(req, segments, part_num)]
|
629
|
+
else:
|
630
|
+
byteranges = [(0, resp_attrs.slo_size - 1)]
|
631
|
+
|
632
|
+
return byteranges
|
633
|
+
|
634
|
+
|
635
|
+
class RespAttrs(object):
|
636
|
+
"""
|
637
|
+
Encapsulate properties of a GET or HEAD response that are pertinent to
|
638
|
+
handling a potential SLO response.
|
639
|
+
|
640
|
+
Instances of this class are typically constructed using the
|
641
|
+
``from_headers`` method.
|
642
|
+
|
643
|
+
:param is_slo: True if the response appears to be an SLO manifest, False
|
644
|
+
otherwise.
|
645
|
+
:param timestamp: an instance of :class:`~swift.common.utils.Timestamp`.
|
646
|
+
:param manifest_etag: the Etag of the manifest object, or None if
|
647
|
+
``is_slo`` is False.
|
648
|
+
:param slo_etag: the Etag of the SLO.
|
649
|
+
:param slo_size: the size of the SLO.
|
650
|
+
"""
|
651
|
+
def __init__(self, is_slo, timestamp, manifest_etag, slo_etag, slo_size):
|
652
|
+
self.is_slo = bool(is_slo)
|
653
|
+
self.timestamp = Timestamp(timestamp or 0)
|
654
|
+
# manifest_etag is unambiguous, but json_md5 is even more explicit
|
655
|
+
self.json_md5 = manifest_etag or ''
|
656
|
+
self.slo_etag = slo_etag or ''
|
657
|
+
try:
|
658
|
+
# even though it's from sysmeta, we have to worry about empty
|
659
|
+
# values - see test_get_invalid_sysmeta_passthrough
|
660
|
+
self.slo_size = int(slo_size)
|
661
|
+
except (ValueError, TypeError):
|
662
|
+
self.slo_size = -1
|
663
|
+
self.is_legacy = not self._has_size_and_etag()
|
664
|
+
|
665
|
+
def _has_size_and_etag(self):
|
666
|
+
return self.slo_size >= 0 and self.slo_etag
|
667
|
+
|
668
|
+
@classmethod
|
669
|
+
def from_headers(cls, response_headers):
|
670
|
+
"""
|
671
|
+
Inspect response headers and extract any resp_attrs we can find.
|
672
|
+
|
673
|
+
:param response_headers: list of tuples from a object response
|
674
|
+
:returns: an instance of RespAttrs to represent the response headers
|
675
|
+
"""
|
676
|
+
is_slo = False
|
677
|
+
timestamp = None
|
678
|
+
found_etag = None
|
679
|
+
slo_etag = None
|
680
|
+
slo_size = None
|
681
|
+
for header, value in response_headers:
|
682
|
+
header = header.lower()
|
683
|
+
if header == 'x-static-large-object':
|
684
|
+
is_slo = config_true_value(value)
|
685
|
+
elif header == 'x-backend-timestamp':
|
686
|
+
timestamp = value
|
687
|
+
elif header == 'etag':
|
688
|
+
found_etag = value
|
689
|
+
elif header == SYSMETA_SLO_ETAG:
|
690
|
+
slo_etag = value
|
691
|
+
elif header == SYSMETA_SLO_SIZE:
|
692
|
+
slo_size = value
|
693
|
+
manifest_etag = found_etag if is_slo else None
|
694
|
+
return cls(is_slo, timestamp, manifest_etag, slo_etag, slo_size)
|
695
|
+
|
696
|
+
def update_from_segments(self, segments):
|
697
|
+
"""
|
698
|
+
Always called if SLO has fetched the manifest response body, for
|
699
|
+
legacy manifests we'll calculate size/etag values we wouldn't have
|
700
|
+
gotten from sys-meta headers.
|
701
|
+
"""
|
702
|
+
# we only have to set size/etag once; it doesn't matter if we got the
|
703
|
+
# values from sysmeta headers or segments
|
704
|
+
if self._has_size_and_etag():
|
705
|
+
return
|
706
|
+
|
707
|
+
calculated_size = 0
|
708
|
+
calculated_etag = md5(usedforsecurity=False)
|
709
|
+
|
710
|
+
for seg_dict in segments:
|
711
|
+
calculated_size += seg_dict['segment_length']
|
712
|
+
|
713
|
+
if 'raw_data' in seg_dict:
|
714
|
+
r = md5(seg_dict['raw_data'],
|
715
|
+
usedforsecurity=False).hexdigest()
|
716
|
+
elif seg_dict.get('range'):
|
717
|
+
r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
|
718
|
+
else:
|
719
|
+
r = seg_dict['hash']
|
720
|
+
calculated_etag.update(r.encode('ascii'))
|
721
|
+
|
722
|
+
self.slo_size = calculated_size
|
723
|
+
self.slo_etag = calculated_etag.hexdigest()
|
724
|
+
|
725
|
+
|
520
726
|
class SloGetContext(WSGIContext):
|
521
727
|
|
522
728
|
max_slo_recursion_depth = 10
|
@@ -524,6 +730,8 @@ class SloGetContext(WSGIContext):
|
|
524
730
|
def __init__(self, slo):
|
525
731
|
self.slo = slo
|
526
732
|
super(SloGetContext, self).__init__(slo.app)
|
733
|
+
# we'll know more after we look at the response metadata
|
734
|
+
self.segment_listing_needed = False
|
527
735
|
|
528
736
|
def _fetch_sub_slo_segments(self, req, version, acc, con, obj):
|
529
737
|
"""
|
@@ -544,19 +752,23 @@ class SloGetContext(WSGIContext):
|
|
544
752
|
method='GET',
|
545
753
|
headers={'x-auth-token': req.headers.get('x-auth-token')},
|
546
754
|
agent='%(orig)s SLO MultipartGET', swift_source='SLO')
|
755
|
+
params_copy = dict(req.params)
|
756
|
+
params_copy.pop('part-number', None)
|
757
|
+
sub_req.params = params_copy
|
547
758
|
sub_resp = sub_req.get_response(self.slo.app)
|
548
759
|
|
549
760
|
if not sub_resp.is_success:
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
761
|
+
# Error message should be short
|
762
|
+
body = sub_resp.body.decode('utf-8')
|
763
|
+
msg = ('while fetching %s, GET of submanifest %s '
|
764
|
+
'failed with status %d (%s)')
|
765
|
+
raise ListingIterError(msg % (
|
766
|
+
req.path, sub_req.path, sub_resp.status_int,
|
767
|
+
body if len(body) <= 60 else body[:57] + '...'))
|
555
768
|
|
556
769
|
try:
|
557
|
-
|
558
|
-
|
559
|
-
except ValueError as err:
|
770
|
+
return self._parse_segments(sub_resp.app_iter)
|
771
|
+
except HTTPException as err:
|
560
772
|
raise ListingIterError(
|
561
773
|
'while fetching %s, JSON-decoding of submanifest %s '
|
562
774
|
'failed with %s' % (req.path, sub_req.path, err))
|
@@ -567,32 +779,8 @@ class SloGetContext(WSGIContext):
|
|
567
779
|
conobj=seg_dict['name'].lstrip('/')
|
568
780
|
)
|
569
781
|
|
570
|
-
def _segment_length(self, seg_dict):
|
571
|
-
"""
|
572
|
-
Returns the number of bytes that will be fetched from the specified
|
573
|
-
segment on a plain GET request for this SLO manifest.
|
574
|
-
"""
|
575
|
-
if 'raw_data' in seg_dict:
|
576
|
-
return len(seg_dict['raw_data'])
|
577
|
-
|
578
|
-
seg_range = seg_dict.get('range')
|
579
|
-
if seg_range is not None:
|
580
|
-
# The range is of the form N-M, where N and M are both positive
|
581
|
-
# decimal integers. We know this because this middleware is the
|
582
|
-
# only thing that creates the SLO manifests stored in the
|
583
|
-
# cluster.
|
584
|
-
range_start, range_end = [int(x) for x in seg_range.split('-')]
|
585
|
-
return (range_end - range_start) + 1
|
586
|
-
else:
|
587
|
-
return int(seg_dict['bytes'])
|
588
|
-
|
589
782
|
def _segment_listing_iterator(self, req, version, account, segments,
|
590
783
|
byteranges):
|
591
|
-
for seg_dict in segments:
|
592
|
-
if config_true_value(seg_dict.get('sub_slo')):
|
593
|
-
override_bytes_from_content_type(seg_dict,
|
594
|
-
logger=self.slo.logger)
|
595
|
-
|
596
784
|
# We handle the range stuff here so that we can be smart about
|
597
785
|
# skipping unused submanifests. For example, if our first segment is a
|
598
786
|
# submanifest referencing 50 MiB total, but start_byte falls in
|
@@ -600,9 +788,6 @@ class SloGetContext(WSGIContext):
|
|
600
788
|
#
|
601
789
|
# If we were to make SegmentedIterable handle all the range
|
602
790
|
# calculations, we would be unable to make this optimization.
|
603
|
-
total_length = sum(self._segment_length(seg) for seg in segments)
|
604
|
-
if not byteranges:
|
605
|
-
byteranges = [(0, total_length - 1)]
|
606
791
|
|
607
792
|
# Cache segments from sub-SLOs in case more than one byterange
|
608
793
|
# includes data from a particular sub-SLO. We only cache a few sets
|
@@ -629,12 +814,26 @@ class SloGetContext(WSGIContext):
|
|
629
814
|
first_byte, last_byte,
|
630
815
|
cached_fetch_sub_slo_segments,
|
631
816
|
recursion_depth=1):
|
817
|
+
"""
|
818
|
+
Iterable that generates a filtered and annotated stream of segment
|
819
|
+
dicts describing the sub-segment ranges that would be used by the
|
820
|
+
SegmentedIterable to construct the bytes for a ranged response.
|
821
|
+
|
822
|
+
:param req: original request object
|
823
|
+
:param version: version
|
824
|
+
:param account: account
|
825
|
+
:param segments: segments dictionary
|
826
|
+
:param first_byte: offset into the large object for the first byte
|
827
|
+
that is returned to the client
|
828
|
+
:param last_byte: offset into the large object for the last byte
|
829
|
+
that is returned to the client
|
830
|
+
:param cached_fetch_sub_slo_segments: LRU cache used for fetching
|
831
|
+
sub-segments
|
832
|
+
:param recursion_depth: max number of recursive sub_slo calls
|
833
|
+
"""
|
632
834
|
last_sub_path = None
|
633
835
|
for seg_dict in segments:
|
634
|
-
|
635
|
-
seg_dict['raw_data'] = strict_b64decode(seg_dict.pop('data'))
|
636
|
-
|
637
|
-
seg_length = self._segment_length(seg_dict)
|
836
|
+
seg_length = seg_dict['segment_length']
|
638
837
|
if first_byte >= seg_length:
|
639
838
|
# don't need any bytes from this segment
|
640
839
|
first_byte -= seg_length
|
@@ -670,10 +869,7 @@ class SloGetContext(WSGIContext):
|
|
670
869
|
"While processing manifest %r, "
|
671
870
|
"max recursion depth was exceeded" % req.path)
|
672
871
|
|
673
|
-
|
674
|
-
sub_path = get_valid_utf8_str(seg_dict['name'])
|
675
|
-
else:
|
676
|
-
sub_path = seg_dict['name']
|
872
|
+
sub_path = seg_dict['name']
|
677
873
|
sub_cont, sub_obj = split_path(sub_path, 2, 2, True)
|
678
874
|
if last_sub_path != sub_path:
|
679
875
|
sub_segments = cached_fetch_sub_slo_segments(
|
@@ -692,8 +888,6 @@ class SloGetContext(WSGIContext):
|
|
692
888
|
recursion_depth=recursion_depth + 1):
|
693
889
|
yield sub_seg_dict
|
694
890
|
else:
|
695
|
-
if six.PY2 and isinstance(seg_dict['name'], six.text_type):
|
696
|
-
seg_dict['name'] = seg_dict['name'].encode("utf-8")
|
697
891
|
yield dict(seg_dict,
|
698
892
|
first_byte=max(0, first_byte) + range_start,
|
699
893
|
last_byte=min(range_end, range_start + last_byte))
|
@@ -701,50 +895,221 @@ class SloGetContext(WSGIContext):
|
|
701
895
|
first_byte -= seg_length
|
702
896
|
last_byte -= seg_length
|
703
897
|
|
704
|
-
def
|
898
|
+
def _is_body_complete(self):
|
899
|
+
content_range = ''
|
900
|
+
for header, value in self._response_headers:
|
901
|
+
if header.lower() == 'content-range':
|
902
|
+
content_range = value
|
903
|
+
break
|
904
|
+
# e.g. Content-Range: bytes 0-14289/14290
|
905
|
+
match = re.match(r'bytes (\d+)-(\d+)/(\d+)$', content_range)
|
906
|
+
if not match:
|
907
|
+
# Malformed or missing, so we don't know what we got.
|
908
|
+
return False
|
909
|
+
first_byte, last_byte, length = [int(x) for x in match.groups()]
|
910
|
+
# If and only if we actually got back the full manifest body, then
|
911
|
+
# we can avoid re-fetching the object.
|
912
|
+
return first_byte == 0 and last_byte == length - 1
|
913
|
+
|
914
|
+
def _need_to_refetch_manifest(self, req, resp_attrs, is_part_num_request):
|
705
915
|
"""
|
706
|
-
|
707
|
-
|
708
|
-
doesn't, we need to make a second request to actually get the whole
|
709
|
-
thing.
|
916
|
+
Check if the segments will be needed to service the request and update
|
917
|
+
the segment_listing_needed attribute.
|
710
918
|
|
711
|
-
|
919
|
+
:return: boolean indicating if we need to refetch, only if the segments
|
920
|
+
ARE needed we MAY need to refetch them!
|
712
921
|
"""
|
713
922
|
if req.method == 'HEAD':
|
714
|
-
#
|
715
|
-
#
|
716
|
-
|
923
|
+
# There may be some cases in the future where a HEAD resp on even a
|
924
|
+
# modern manifest should refetch, e.g. lp bug #2029174
|
925
|
+
self.segment_listing_needed = (resp_attrs.is_legacy or
|
926
|
+
is_part_num_request)
|
927
|
+
# it will always be the case that a HEAD must re-fetch iff
|
928
|
+
# segment_listing_needed
|
929
|
+
return self.segment_listing_needed
|
930
|
+
|
931
|
+
last_resp_status_int = self._get_status_int()
|
932
|
+
# These are based on etag (or last-modified), but the SLO's etag is
|
933
|
+
# almost certainly not the manifest object's etag. Still, it's highly
|
934
|
+
# likely that the submitted If-None-Match won't match the manifest
|
935
|
+
# object's etag, so we can avoid re-fetching the manifest if we got a
|
936
|
+
# successful response.
|
937
|
+
if last_resp_status_int in (412, 304):
|
938
|
+
# a conditional response from a modern manifest would have an
|
939
|
+
# accurate SLO etag, AND comparison with the etag-is-at header, but
|
940
|
+
# for legacy manifests responses (who always need to calculate the
|
941
|
+
# correct etag, even for if-[un]modified-since errors) we can't say
|
942
|
+
# what the etag is or if it matches unless we calculate it from
|
943
|
+
# segments - so we always need them
|
944
|
+
self.segment_listing_needed = resp_attrs.is_legacy
|
945
|
+
# if we need them; we can't get them from the error
|
946
|
+
return self.segment_listing_needed
|
947
|
+
|
948
|
+
# This is GET request for an SLO object, if we're going to return a
|
949
|
+
# successful response we're going to need the segments, but this
|
950
|
+
# resp_iter may not contain the entire SLO manifest.
|
951
|
+
self.segment_listing_needed = True
|
952
|
+
|
953
|
+
# modern swift object-servers should ignore Range headers on manifests,
|
954
|
+
# but during upgrade if we get a range response we'll probably have to
|
955
|
+
# refetch
|
956
|
+
if last_resp_status_int == 416:
|
957
|
+
# if the range wasn't satisfiable we need to refetch
|
717
958
|
return True
|
959
|
+
elif last_resp_status_int == 206:
|
960
|
+
# a partial response might included the whole content-range?!
|
961
|
+
return not self._is_body_complete()
|
962
|
+
else:
|
963
|
+
# a good number of error responses would have returned earlier for
|
964
|
+
# lacking is_slo sys-meta, at this point we've filtered all the
|
965
|
+
# other response codes, so this is a prefectly normal 200 response,
|
966
|
+
# no need to refetch
|
967
|
+
return False
|
968
|
+
|
969
|
+
def _refetch_manifest(self, req, resp_iter, orig_resp_attrs):
|
970
|
+
req.environ['swift.non_client_disconnect'] = True
|
971
|
+
friendly_close(resp_iter)
|
972
|
+
del req.environ['swift.non_client_disconnect']
|
973
|
+
|
974
|
+
headers_subset = ['x-auth-token', 'x-open-expired']
|
975
|
+
get_req = make_subrequest(
|
976
|
+
req.environ, method='GET',
|
977
|
+
headers={k: req.headers.get(k)
|
978
|
+
for k in headers_subset if k in req.headers},
|
979
|
+
agent='%(orig)s SLO MultipartGET', swift_source='SLO')
|
980
|
+
resp_iter = self._app_call(get_req.environ)
|
981
|
+
new_resp_attrs = RespAttrs.from_headers(self._response_headers)
|
982
|
+
if new_resp_attrs.timestamp < orig_resp_attrs.timestamp and \
|
983
|
+
not new_resp_attrs.is_slo:
|
984
|
+
# Our *orig_resp_attrs* saw *newer* data that indicated it was an
|
985
|
+
# SLO, but on refetch it's an older object or error; 503 seems
|
986
|
+
# reasonable?
|
987
|
+
friendly_close(resp_iter)
|
988
|
+
raise HTTPServiceUnavailable(request=req)
|
989
|
+
# else, the caller will know how to return this response
|
990
|
+
return new_resp_attrs, resp_iter
|
991
|
+
|
992
|
+
def _parse_segments(self, resp_iter):
|
993
|
+
"""
|
994
|
+
Read the manifest body and parse segments.
|
718
995
|
|
719
|
-
|
996
|
+
:returns: segments
|
997
|
+
:raises: HTTPServerError
|
998
|
+
"""
|
999
|
+
segments = self._get_manifest_read(resp_iter)
|
1000
|
+
_annotate_segments(segments, logger=self.slo.logger)
|
1001
|
+
return segments
|
720
1002
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
1003
|
+
def _return_manifest_response(self, req, start_response, resp_iter,
|
1004
|
+
is_format_raw):
|
1005
|
+
if is_format_raw:
|
1006
|
+
json_data = self.convert_segment_listing(resp_iter)
|
1007
|
+
# we've created a new response body
|
1008
|
+
resp_iter = [json_data]
|
1009
|
+
replace_headers = {
|
1010
|
+
# Note that we have to return the large object's content-type
|
1011
|
+
# (not application/json) so it's like what the client sent on
|
1012
|
+
# PUT. Otherwise, server-side copy won't work.
|
1013
|
+
'Content-Length': len(json_data),
|
1014
|
+
'Etag': md5(json_data, usedforsecurity=False).hexdigest(),
|
1015
|
+
}
|
1016
|
+
else:
|
1017
|
+
# we're going to return the manifest resp_iter as-is
|
1018
|
+
replace_headers = {
|
1019
|
+
'Content-Type': 'application/json; charset=utf-8',
|
1020
|
+
}
|
1021
|
+
return self._return_response(req, start_response, resp_iter,
|
1022
|
+
replace_headers)
|
1023
|
+
|
1024
|
+
def _return_slo_response(self, req, start_response, resp_iter, resp_attrs):
|
1025
|
+
headers = {
|
1026
|
+
'Etag': '"%s"' % resp_attrs.slo_etag,
|
1027
|
+
'X-Manifest-Etag': resp_attrs.json_md5,
|
1028
|
+
# swob will fix this for a GET with Range
|
1029
|
+
'Content-Length': str(resp_attrs.slo_size),
|
1030
|
+
# ignore bogus content-range, make swob figure it out
|
1031
|
+
'Content-Range': None,
|
1032
|
+
}
|
1033
|
+
if self.segment_listing_needed:
|
1034
|
+
# consume existing resp_iter; we'll create a new one
|
1035
|
+
segments = self._parse_segments(resp_iter)
|
1036
|
+
resp_attrs.update_from_segments(segments)
|
1037
|
+
headers['Etag'] = '"%s"' % resp_attrs.slo_etag
|
1038
|
+
headers['Content-Length'] = str(resp_attrs.slo_size)
|
1039
|
+
part_num = get_valid_part_num(req)
|
1040
|
+
if part_num:
|
1041
|
+
headers['X-Parts-Count'] = len(segments)
|
1042
|
+
|
1043
|
+
if part_num and part_num > len(segments):
|
1044
|
+
if req.method == 'HEAD':
|
1045
|
+
resp_iter = []
|
1046
|
+
headers['Content-Length'] = '0'
|
1047
|
+
else:
|
1048
|
+
body = b'The requested part number is not satisfiable'
|
1049
|
+
resp_iter = [body]
|
1050
|
+
headers['Content-Length'] = len(body)
|
1051
|
+
headers['Content-Range'] = 'bytes */%d' % resp_attrs.slo_size
|
1052
|
+
self._response_status = '416 Requested Range Not Satisfiable'
|
1053
|
+
elif part_num and req.method == 'HEAD':
|
1054
|
+
resp_iter = []
|
1055
|
+
headers['Content-Length'] = \
|
1056
|
+
segments[part_num - 1].get('segment_length')
|
1057
|
+
start, end = calculate_byterange_for_part_num(
|
1058
|
+
req, segments, part_num)
|
1059
|
+
headers['Content-Range'] = \
|
1060
|
+
'bytes {}-{}/{}'.format(start, end,
|
1061
|
+
resp_attrs.slo_size)
|
1062
|
+
# The RFC specifies 206 in the context of Range requests, and
|
1063
|
+
# Range headers MUST be ignored for HEADs [1], so a HEAD will
|
1064
|
+
# not normally return a 206. However, a part-number HEAD
|
1065
|
+
# returns Content-Length equal to the part size, rather than
|
1066
|
+
# the whole object size, so in this case we do return 206.
|
1067
|
+
# [1] https://www.rfc-editor.org/rfc/rfc9110#name-range
|
1068
|
+
self._response_status = '206 Partial Content'
|
1069
|
+
elif req.method == 'HEAD':
|
1070
|
+
resp_iter = []
|
1071
|
+
else:
|
1072
|
+
byteranges = calculate_byteranges(
|
1073
|
+
req, segments, resp_attrs, part_num)
|
1074
|
+
resp_iter = self._build_resp_iter(req, segments, byteranges)
|
1075
|
+
return self._return_response(req, start_response, resp_iter,
|
1076
|
+
replace_headers=headers)
|
1077
|
+
|
1078
|
+
def _return_response(self, req, start_response, resp_iter,
|
1079
|
+
replace_headers):
|
1080
|
+
if req.method == 'HEAD' or self._get_status_int() in (412, 304):
|
1081
|
+
# we should drain HEAD and unmet condition responses since they
|
1082
|
+
# don't have bodies
|
1083
|
+
friendly_close(resp_iter)
|
1084
|
+
resp_iter = b''
|
1085
|
+
resp_headers = HeaderKeyDict(self._response_headers, **replace_headers)
|
1086
|
+
resp = Response(
|
1087
|
+
status=self._response_status,
|
1088
|
+
headers=resp_headers,
|
1089
|
+
app_iter=resp_iter,
|
1090
|
+
request=req,
|
1091
|
+
conditional_response=True,
|
1092
|
+
conditional_etag=resolve_etag_is_at_header(req, resp_headers))
|
1093
|
+
return resp(req.environ, start_response)
|
729
1094
|
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
return
|
1095
|
+
def _return_non_slo_response(self, req, start_response, resp_iter):
|
1096
|
+
# our "pass-through" response may have been from a manifest refetch w/o
|
1097
|
+
# range/conditional headers that turned out to be a real object, and
|
1098
|
+
# now we want out. But if the original client request included Range
|
1099
|
+
# or Conditional headers we can trust swob to do the right conversion
|
1100
|
+
# back into a 206/416/304/412 (as long as the response we have is a
|
1101
|
+
# normal successful response and we respect any forwarding middleware's
|
1102
|
+
# etag-is-at header that we stripped off for the refetch!)
|
1103
|
+
resp = Response(
|
1104
|
+
status=self._response_status,
|
1105
|
+
headers=self._response_headers,
|
1106
|
+
app_iter=resp_iter,
|
1107
|
+
request=req,
|
1108
|
+
conditional_response=self._get_status_int() == 200,
|
1109
|
+
conditional_etag=resolve_etag_is_at_header(
|
1110
|
+
req, self._response_headers)
|
1111
|
+
)
|
1112
|
+
return resp(req.environ, start_response)
|
748
1113
|
|
749
1114
|
def handle_slo_get_or_head(self, req, start_response):
|
750
1115
|
"""
|
@@ -757,134 +1122,72 @@ class SloGetContext(WSGIContext):
|
|
757
1122
|
large object manifest.
|
758
1123
|
:param start_response: WSGI start_response callable
|
759
1124
|
"""
|
760
|
-
|
1125
|
+
is_manifest_get = get_param(req, 'multipart-manifest') == 'get'
|
1126
|
+
is_format_raw = is_manifest_get and get_param(req, 'format') == 'raw'
|
1127
|
+
|
1128
|
+
if not is_manifest_get:
|
761
1129
|
# If this object is an SLO manifest, we may have saved off the
|
762
1130
|
# large object etag during the original PUT. Send an
|
763
|
-
# X-Backend-Etag-Is-At header so that, if the SLO etag *was*
|
764
|
-
#
|
765
|
-
#
|
1131
|
+
# X-Backend-Etag-Is-At header so that, if the SLO etag *was* saved,
|
1132
|
+
# we can trust the object-server to respond appropriately to
|
1133
|
+
# If-Match/If-None-Match requests.
|
766
1134
|
update_etag_is_at_header(req, SYSMETA_SLO_ETAG)
|
767
|
-
|
1135
|
+
# Tell the object server that if it's a manifest,
|
1136
|
+
# we want the whole thing
|
1137
|
+
update_ignore_range_header(req, 'X-Static-Large-Object')
|
768
1138
|
|
769
|
-
#
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
#
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
req.if_match or req.if_none_match)
|
814
|
-
if slo_etag and slo_size and (
|
815
|
-
req.method == 'HEAD' or is_conditional):
|
816
|
-
# Since we have length and etag, we can respond immediately
|
817
|
-
resp = Response(
|
818
|
-
status=self._response_status,
|
819
|
-
headers=self._response_headers,
|
820
|
-
app_iter=resp_iter,
|
821
|
-
request=req,
|
822
|
-
conditional_etag=resolve_etag_is_at_header(
|
823
|
-
req, self._response_headers),
|
824
|
-
conditional_response=True)
|
825
|
-
resp.headers.update({
|
826
|
-
'Etag': '"%s"' % slo_etag,
|
827
|
-
'X-Manifest-Etag': self._response_header_value('etag'),
|
828
|
-
'Content-Length': slo_size,
|
829
|
-
})
|
830
|
-
return resp(req.environ, start_response)
|
831
|
-
|
832
|
-
if self._need_to_refetch_manifest(req):
|
833
|
-
req.environ['swift.non_client_disconnect'] = True
|
834
|
-
close_if_possible(resp_iter)
|
835
|
-
del req.environ['swift.non_client_disconnect']
|
836
|
-
|
837
|
-
get_req = make_subrequest(
|
838
|
-
req.environ, method='GET',
|
839
|
-
headers={'x-auth-token': req.headers.get('x-auth-token')},
|
840
|
-
agent='%(orig)s SLO MultipartGET', swift_source='SLO')
|
841
|
-
resp_iter = self._app_call(get_req.environ)
|
842
|
-
slo_marker = config_true_value(self._response_header_value(
|
843
|
-
'x-static-large-object'))
|
844
|
-
if not slo_marker: # will also catch non-2xx responses
|
845
|
-
got_timestamp = self._response_header_value(
|
846
|
-
'x-backend-timestamp') or '0'
|
847
|
-
if Timestamp(got_timestamp) >= Timestamp(slo_timestamp):
|
848
|
-
# We've got a newer response available, so serve that.
|
849
|
-
# Note that if there's data, it's going to be a 200 now,
|
850
|
-
# not a 206, and we're not going to drop bytes in the
|
851
|
-
# proxy on the client's behalf. Fortunately, the RFC is
|
852
|
-
# pretty forgiving for a server; there's no guarantee that
|
853
|
-
# a Range header will be respected.
|
854
|
-
resp = Response(
|
855
|
-
status=self._response_status,
|
856
|
-
headers=self._response_headers,
|
857
|
-
app_iter=resp_iter,
|
858
|
-
request=req,
|
859
|
-
conditional_etag=resolve_etag_is_at_header(
|
860
|
-
req, self._response_headers),
|
861
|
-
conditional_response=is_success(
|
862
|
-
int(self._response_status[:3])))
|
863
|
-
return resp(req.environ, start_response)
|
864
|
-
else:
|
865
|
-
# We saw newer data that indicated it's an SLO, but
|
866
|
-
# couldn't fetch the whole thing; 503 seems reasonable?
|
867
|
-
close_if_possible(resp_iter)
|
868
|
-
raise HTTPServiceUnavailable(request=req)
|
869
|
-
# NB: we might have gotten an out-of-date manifest -- that's OK;
|
870
|
-
# we'll just try to serve the old data
|
871
|
-
|
872
|
-
# Any Content-Range from a manifest is almost certainly wrong for the
|
873
|
-
# full large object.
|
874
|
-
resp_headers = [(h, v) for h, v in self._response_headers
|
875
|
-
if not h.lower() == 'content-range']
|
876
|
-
|
877
|
-
response = self.get_or_head_response(
|
878
|
-
req, resp_headers, resp_iter)
|
879
|
-
return response(req.environ, start_response)
|
880
|
-
|
881
|
-
def convert_segment_listing(self, resp_headers, resp_iter):
|
1139
|
+
# process original request
|
1140
|
+
orig_path_info = req.path_info
|
1141
|
+
resp_iter = self._app_call(req.environ)
|
1142
|
+
resp_attrs = RespAttrs.from_headers(self._response_headers)
|
1143
|
+
if resp_attrs.is_slo and not is_manifest_get:
|
1144
|
+
try:
|
1145
|
+
# only validate part-number if the request is to an SLO
|
1146
|
+
part_num = get_valid_part_num(req)
|
1147
|
+
except HTTPException:
|
1148
|
+
friendly_close(resp_iter)
|
1149
|
+
raise
|
1150
|
+
# the next two calls hide a couple side effects, sorry:
|
1151
|
+
#
|
1152
|
+
# 1) regardless of the return value the "need_to_refetch" check
|
1153
|
+
# *may* also set self.segment_listing_needed = True (it's
|
1154
|
+
# commented to help you wrap your head around that one,
|
1155
|
+
# good luck)
|
1156
|
+
# 2) if we refetch, we overwrite the current resp_iter and
|
1157
|
+
# resp_attrs variables, partly because we *might* get back a NOT
|
1158
|
+
# resp_attrs.is_slo response (even if we had one to start), but
|
1159
|
+
# hopefully they're just the manifest resp we needed to refetch!
|
1160
|
+
if self._need_to_refetch_manifest(req, resp_attrs, part_num):
|
1161
|
+
# reset path in case it was modified during original request
|
1162
|
+
# (e.g. object versioning might re-write the path)
|
1163
|
+
req.path_info = orig_path_info
|
1164
|
+
resp_attrs, resp_iter = self._refetch_manifest(
|
1165
|
+
req, resp_iter, resp_attrs)
|
1166
|
+
|
1167
|
+
if not resp_attrs.is_slo:
|
1168
|
+
# even if the original resp_attrs may have been SLO we may have
|
1169
|
+
# refetched, this also handles the server error case
|
1170
|
+
return self._return_non_slo_response(
|
1171
|
+
req, start_response, resp_iter)
|
1172
|
+
|
1173
|
+
if is_manifest_get:
|
1174
|
+
# manifest pass through doesn't require resp_attrs
|
1175
|
+
return self._return_manifest_response(req, start_response,
|
1176
|
+
resp_iter, is_format_raw)
|
1177
|
+
|
1178
|
+
# this a GET/HEAD response for the SLO object (not the manifest)
|
1179
|
+
return self._return_slo_response(req, start_response, resp_iter,
|
1180
|
+
resp_attrs)
|
1181
|
+
|
1182
|
+
def convert_segment_listing(self, resp_iter):
|
882
1183
|
"""
|
883
1184
|
Converts the manifest data to match with the format
|
884
1185
|
that was put in through ?multipart-manifest=put
|
885
1186
|
|
886
|
-
:param resp_headers: response headers
|
887
1187
|
:param resp_iter: a response iterable
|
1188
|
+
|
1189
|
+
:raises HTTPServerError:
|
1190
|
+
:returns: the json-serialized raw format (as bytes)
|
888
1191
|
"""
|
889
1192
|
segments = self._get_manifest_read(resp_iter)
|
890
1193
|
|
@@ -898,107 +1201,30 @@ class SloGetContext(WSGIContext):
|
|
898
1201
|
seg_dict['size_bytes'] = seg_dict.pop('bytes', None)
|
899
1202
|
seg_dict['etag'] = seg_dict.pop('hash', None)
|
900
1203
|
|
901
|
-
json_data = json.dumps(segments) # convert to string
|
902
|
-
|
903
|
-
json_data = json_data.encode('utf-8')
|
904
|
-
|
905
|
-
new_headers = []
|
906
|
-
for header, value in resp_headers:
|
907
|
-
if header.lower() == 'content-length':
|
908
|
-
new_headers.append(('Content-Length', len(json_data)))
|
909
|
-
else:
|
910
|
-
new_headers.append((header, value))
|
911
|
-
self._response_headers = new_headers
|
912
|
-
|
913
|
-
return [json_data]
|
1204
|
+
json_data = json.dumps(segments, sort_keys=True) # convert to string
|
1205
|
+
return json_data.encode('utf-8')
|
914
1206
|
|
915
1207
|
def _get_manifest_read(self, resp_iter):
|
916
1208
|
with closing_if_possible(resp_iter):
|
917
1209
|
resp_body = b''.join(resp_iter)
|
918
1210
|
try:
|
919
1211
|
segments = json.loads(resp_body)
|
920
|
-
except ValueError:
|
921
|
-
|
922
|
-
|
1212
|
+
except ValueError as e:
|
1213
|
+
msg = 'Unable to load SLO manifest'
|
1214
|
+
self.slo.logger.error('%s: %s', msg, e)
|
1215
|
+
raise HTTPServerError(msg)
|
923
1216
|
return segments
|
924
1217
|
|
925
|
-
def
|
926
|
-
|
927
|
-
|
928
|
-
content_length = None
|
929
|
-
response_headers = []
|
930
|
-
for header, value in resp_headers:
|
931
|
-
lheader = header.lower()
|
932
|
-
if lheader == 'etag':
|
933
|
-
response_headers.append(('X-Manifest-Etag', value))
|
934
|
-
elif lheader != 'content-length':
|
935
|
-
response_headers.append((header, value))
|
936
|
-
|
937
|
-
if lheader == SYSMETA_SLO_ETAG:
|
938
|
-
slo_etag = value
|
939
|
-
elif lheader == SYSMETA_SLO_SIZE:
|
940
|
-
# it's from sysmeta, so we don't worry about non-integer
|
941
|
-
# values here
|
942
|
-
content_length = int(value)
|
943
|
-
|
944
|
-
# Prep to calculate content_length & etag if necessary
|
945
|
-
if slo_etag is None:
|
946
|
-
calculated_etag = md5()
|
947
|
-
if content_length is None:
|
948
|
-
calculated_content_length = 0
|
949
|
-
|
950
|
-
for seg_dict in segments:
|
951
|
-
# Decode any inlined data; it's important that we do this *before*
|
952
|
-
# calculating the segment length and etag
|
953
|
-
if 'data' in seg_dict:
|
954
|
-
seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
|
955
|
-
|
956
|
-
if slo_etag is None:
|
957
|
-
if 'raw_data' in seg_dict:
|
958
|
-
r = md5(seg_dict['raw_data']).hexdigest()
|
959
|
-
elif seg_dict.get('range'):
|
960
|
-
r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
|
961
|
-
else:
|
962
|
-
r = seg_dict['hash']
|
963
|
-
calculated_etag.update(r.encode('ascii'))
|
964
|
-
|
965
|
-
if content_length is None:
|
966
|
-
if config_true_value(seg_dict.get('sub_slo')):
|
967
|
-
override_bytes_from_content_type(
|
968
|
-
seg_dict, logger=self.slo.logger)
|
969
|
-
calculated_content_length += self._segment_length(seg_dict)
|
970
|
-
|
971
|
-
if slo_etag is None:
|
972
|
-
slo_etag = calculated_etag.hexdigest()
|
973
|
-
if content_length is None:
|
974
|
-
content_length = calculated_content_length
|
1218
|
+
def _build_resp_iter(self, req, segments, byteranges):
|
1219
|
+
"""
|
1220
|
+
Build a response iterable for a GET request.
|
975
1221
|
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
if req.method == 'HEAD':
|
980
|
-
return self._manifest_head_response(req, response_headers)
|
981
|
-
else:
|
982
|
-
return self._manifest_get_response(
|
983
|
-
req, content_length, response_headers, segments)
|
984
|
-
|
985
|
-
def _manifest_head_response(self, req, response_headers):
|
986
|
-
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
987
|
-
return HTTPOk(request=req, headers=response_headers, body=b'',
|
988
|
-
conditional_etag=conditional_etag,
|
989
|
-
conditional_response=True)
|
990
|
-
|
991
|
-
def _manifest_get_response(self, req, content_length, response_headers,
|
992
|
-
segments):
|
993
|
-
if req.range:
|
994
|
-
byteranges = [
|
995
|
-
# For some reason, swob.Range.ranges_for_length adds 1 to the
|
996
|
-
# last byte's position.
|
997
|
-
(start, end - 1) for start, end
|
998
|
-
in req.range.ranges_for_length(content_length)]
|
999
|
-
else:
|
1000
|
-
byteranges = []
|
1222
|
+
:param req: the request object
|
1223
|
+
:param segments: the list of seg_dicts
|
1224
|
+
:param byteranges: a list of tuples representing byteranges
|
1001
1225
|
|
1226
|
+
:returns: a segmented iterable
|
1227
|
+
"""
|
1002
1228
|
ver, account, _junk = req.split_path(3, 3, rest_with_last=True)
|
1003
1229
|
account = wsgi_to_str(account)
|
1004
1230
|
plain_listing_iter = self._segment_listing_iterator(
|
@@ -1042,15 +1268,8 @@ class SloGetContext(WSGIContext):
|
|
1042
1268
|
# their Etag/Content Length no longer match the connection
|
1043
1269
|
# will drop. In this case a 409 Conflict will be logged in
|
1044
1270
|
# the proxy logs and the user will receive incomplete results.
|
1045
|
-
|
1046
|
-
|
1047
|
-
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
1048
|
-
response = Response(request=req, content_length=content_length,
|
1049
|
-
headers=response_headers,
|
1050
|
-
conditional_response=True,
|
1051
|
-
conditional_etag=conditional_etag,
|
1052
|
-
app_iter=segmented_iter)
|
1053
|
-
return response
|
1271
|
+
raise HTTPConflict(request=req)
|
1272
|
+
return segmented_iter
|
1054
1273
|
|
1055
1274
|
|
1056
1275
|
class StaticLargeObject(object):
|
@@ -1077,13 +1296,15 @@ class StaticLargeObject(object):
|
|
1077
1296
|
def __init__(self, app, conf,
|
1078
1297
|
max_manifest_segments=DEFAULT_MAX_MANIFEST_SEGMENTS,
|
1079
1298
|
max_manifest_size=DEFAULT_MAX_MANIFEST_SIZE,
|
1080
|
-
yield_frequency=DEFAULT_YIELD_FREQUENCY
|
1299
|
+
yield_frequency=DEFAULT_YIELD_FREQUENCY,
|
1300
|
+
allow_async_delete=True):
|
1081
1301
|
self.conf = conf
|
1082
1302
|
self.app = app
|
1083
1303
|
self.logger = get_logger(conf, log_route='slo')
|
1084
1304
|
self.max_manifest_segments = max_manifest_segments
|
1085
1305
|
self.max_manifest_size = max_manifest_size
|
1086
1306
|
self.yield_frequency = yield_frequency
|
1307
|
+
self.allow_async_delete = allow_async_delete
|
1087
1308
|
self.max_get_time = int(self.conf.get('max_get_time', 86400))
|
1088
1309
|
self.rate_limit_under_size = int(self.conf.get(
|
1089
1310
|
'rate_limit_under_size', DEFAULT_RATE_LIMIT_UNDER_SIZE))
|
@@ -1101,6 +1322,8 @@ class StaticLargeObject(object):
|
|
1101
1322
|
delete_concurrency=delete_concurrency,
|
1102
1323
|
logger=self.logger)
|
1103
1324
|
|
1325
|
+
self.expirer_config = expirer.ExpirerConfig(conf, logger=self.logger)
|
1326
|
+
|
1104
1327
|
def handle_multipart_get_or_head(self, req, start_response):
|
1105
1328
|
"""
|
1106
1329
|
Handles the GET or HEAD of a SLO manifest.
|
@@ -1164,12 +1387,8 @@ class StaticLargeObject(object):
|
|
1164
1387
|
path2indices[seg_dict['path']].append(index)
|
1165
1388
|
|
1166
1389
|
def do_head(obj_name):
|
1167
|
-
|
1168
|
-
|
1169
|
-
get_valid_utf8_str(obj_name).lstrip('/')])
|
1170
|
-
else:
|
1171
|
-
obj_path = '/'.join(['', vrs, account,
|
1172
|
-
str_to_wsgi(obj_name.lstrip('/'))])
|
1390
|
+
obj_path = '/'.join(['', vrs, account,
|
1391
|
+
str_to_wsgi(obj_name.lstrip('/'))])
|
1173
1392
|
obj_path = wsgi_quote(obj_path)
|
1174
1393
|
|
1175
1394
|
sub_req = make_subrequest(
|
@@ -1312,26 +1531,26 @@ class StaticLargeObject(object):
|
|
1312
1531
|
out_content_type, resp_dict, problem_segments, 'upload')
|
1313
1532
|
return
|
1314
1533
|
|
1315
|
-
slo_etag = md5()
|
1534
|
+
slo_etag = md5(usedforsecurity=False)
|
1316
1535
|
for seg_data in data_for_storage:
|
1317
1536
|
if 'data' in seg_data:
|
1318
1537
|
raw_data = base64.b64decode(seg_data['data'])
|
1319
|
-
r = md5(raw_data).hexdigest()
|
1538
|
+
r = md5(raw_data, usedforsecurity=False).hexdigest()
|
1320
1539
|
elif seg_data.get('range'):
|
1321
1540
|
r = '%s:%s;' % (seg_data['hash'], seg_data['range'])
|
1322
1541
|
else:
|
1323
1542
|
r = seg_data['hash']
|
1324
|
-
slo_etag.update(r.encode('ascii')
|
1543
|
+
slo_etag.update(r.encode('ascii'))
|
1325
1544
|
|
1326
1545
|
slo_etag = slo_etag.hexdigest()
|
1327
|
-
client_etag = req.headers.get('Etag')
|
1328
|
-
if client_etag and client_etag
|
1546
|
+
client_etag = normalize_etag(req.headers.get('Etag'))
|
1547
|
+
if client_etag and client_etag != slo_etag:
|
1329
1548
|
err = HTTPUnprocessableEntity(request=req)
|
1330
1549
|
if heartbeat:
|
1331
1550
|
resp_dict = {}
|
1332
1551
|
resp_dict['Response Status'] = err.status
|
1333
1552
|
err_body = err.body
|
1334
|
-
if
|
1553
|
+
if isinstance(err_body, bytes):
|
1335
1554
|
err_body = err_body.decode('utf-8', errors='replace')
|
1336
1555
|
resp_dict['Response Body'] = err_body or '\n'.join(
|
1337
1556
|
RESPONSE_REASONS.get(err.status_int, ['']))
|
@@ -1343,15 +1562,13 @@ class StaticLargeObject(object):
|
|
1343
1562
|
yield chunk
|
1344
1563
|
return
|
1345
1564
|
|
1346
|
-
json_data = json.dumps(data_for_storage)
|
1347
|
-
if six.PY3:
|
1348
|
-
json_data = json_data.encode('utf-8')
|
1565
|
+
json_data = json.dumps(data_for_storage).encode('utf-8')
|
1349
1566
|
req.body = json_data
|
1350
1567
|
req.headers.update({
|
1351
1568
|
SYSMETA_SLO_ETAG: slo_etag,
|
1352
1569
|
SYSMETA_SLO_SIZE: total_size,
|
1353
1570
|
'X-Static-Large-Object': 'True',
|
1354
|
-
'Etag': md5(json_data).hexdigest(),
|
1571
|
+
'Etag': md5(json_data, usedforsecurity=False).hexdigest(),
|
1355
1572
|
})
|
1356
1573
|
|
1357
1574
|
# Ensure container listings have both etags. However, if any
|
@@ -1380,7 +1597,7 @@ class StaticLargeObject(object):
|
|
1380
1597
|
|
1381
1598
|
if heartbeat:
|
1382
1599
|
resp_body = resp.body
|
1383
|
-
if
|
1600
|
+
if isinstance(resp_body, bytes):
|
1384
1601
|
resp_body = resp_body.decode('utf-8')
|
1385
1602
|
resp_dict['Response Body'] = resp_body
|
1386
1603
|
yield separator + get_response_body(
|
@@ -1406,14 +1623,14 @@ class StaticLargeObject(object):
|
|
1406
1623
|
raise HTTPPreconditionFailed(
|
1407
1624
|
request=req, body='Invalid UTF8 or contains NULL')
|
1408
1625
|
vrs, account, container, obj = req.split_path(4, 4, True)
|
1409
|
-
|
1410
|
-
obj_path = ('/%s/%s' % (container, obj)).decode('utf-8')
|
1411
|
-
else:
|
1412
|
-
obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
|
1626
|
+
obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
|
1413
1627
|
|
1414
1628
|
segments = [{
|
1415
1629
|
'sub_slo': True,
|
1416
1630
|
'name': obj_path}]
|
1631
|
+
if 'version-id' in req.params:
|
1632
|
+
segments[0]['version_id'] = req.params['version-id']
|
1633
|
+
|
1417
1634
|
while segments:
|
1418
1635
|
# We chose not to set the limit at max_manifest_segments
|
1419
1636
|
# in the case this value was decreased by operators.
|
@@ -1434,7 +1651,7 @@ class StaticLargeObject(object):
|
|
1434
1651
|
except HTTPException as err:
|
1435
1652
|
# allow bulk delete response to report errors
|
1436
1653
|
err_body = err.body
|
1437
|
-
if
|
1654
|
+
if isinstance(err_body, bytes):
|
1438
1655
|
err_body = err_body.decode('utf-8', errors='replace')
|
1439
1656
|
seg_data['error'] = {'code': err.status_int,
|
1440
1657
|
'message': err_body}
|
@@ -1443,8 +1660,6 @@ class StaticLargeObject(object):
|
|
1443
1660
|
seg_data['sub_slo'] = False
|
1444
1661
|
segments.append(seg_data)
|
1445
1662
|
else:
|
1446
|
-
if six.PY2:
|
1447
|
-
seg_data['name'] = seg_data['name'].encode('utf-8')
|
1448
1663
|
yield seg_data
|
1449
1664
|
|
1450
1665
|
def get_slo_segments(self, obj_name, req):
|
@@ -1464,22 +1679,29 @@ class StaticLargeObject(object):
|
|
1464
1679
|
vrs, account, _junk = req.split_path(2, 3, True)
|
1465
1680
|
new_env = req.environ.copy()
|
1466
1681
|
new_env['REQUEST_METHOD'] = 'GET'
|
1467
|
-
del
|
1682
|
+
del new_env['wsgi.input']
|
1468
1683
|
new_env['QUERY_STRING'] = 'multipart-manifest=get'
|
1684
|
+
if 'version-id' in req.params:
|
1685
|
+
new_env['QUERY_STRING'] += \
|
1686
|
+
'&version-id=' + req.params['version-id']
|
1469
1687
|
new_env['CONTENT_LENGTH'] = 0
|
1470
1688
|
new_env['HTTP_USER_AGENT'] = \
|
1471
1689
|
'%s MultipartDELETE' % new_env.get('HTTP_USER_AGENT')
|
1472
1690
|
new_env['swift.source'] = 'SLO'
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
1479
|
-
|
1480
|
-
|
1481
|
-
|
1482
|
-
resp
|
1691
|
+
new_env['PATH_INFO'] = (
|
1692
|
+
'/%s/%s/%s' % (vrs, account, str_to_wsgi(obj_name.lstrip('/')))
|
1693
|
+
)
|
1694
|
+
# Just request the last byte of non-SLO objects so we don't waste
|
1695
|
+
# a resources in friendly_close() below
|
1696
|
+
manifest_req = Request.blank('', new_env, range='bytes=-1')
|
1697
|
+
update_ignore_range_header(manifest_req, 'X-Static-Large-Object')
|
1698
|
+
resp = manifest_req.get_response(self.app)
|
1699
|
+
|
1700
|
+
if resp.is_success and config_true_value(resp.headers.get(
|
1701
|
+
'X-Static-Large-Object')) and len(resp.body) == 1:
|
1702
|
+
# pre-2.24.0 object-server
|
1703
|
+
manifest_req = Request.blank('', new_env)
|
1704
|
+
resp = manifest_req.get_response(self.app)
|
1483
1705
|
|
1484
1706
|
if resp.is_success:
|
1485
1707
|
if config_true_value(resp.headers.get('X-Static-Large-Object')):
|
@@ -1488,6 +1710,8 @@ class StaticLargeObject(object):
|
|
1488
1710
|
except ValueError:
|
1489
1711
|
raise HTTPServerError('Unable to load SLO manifest')
|
1490
1712
|
else:
|
1713
|
+
# Drain and close GET request (prevents socket leaks)
|
1714
|
+
friendly_close(resp)
|
1491
1715
|
raise HTTPBadRequest('Not an SLO manifest')
|
1492
1716
|
elif resp.status_int == HTTP_NOT_FOUND:
|
1493
1717
|
raise HTTPNotFound('SLO manifest not found')
|
@@ -1496,6 +1720,81 @@ class StaticLargeObject(object):
|
|
1496
1720
|
else:
|
1497
1721
|
raise HTTPServerError('Unable to load SLO manifest or segment.')
|
1498
1722
|
|
1723
|
+
def handle_async_delete(self, req):
|
1724
|
+
if not check_utf8(wsgi_to_str(req.path_info)):
|
1725
|
+
raise HTTPPreconditionFailed(
|
1726
|
+
request=req, body='Invalid UTF8 or contains NULL')
|
1727
|
+
vrs, account, container, obj = req.split_path(4, 4, True)
|
1728
|
+
obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
|
1729
|
+
segments = [seg for seg in self.get_slo_segments(obj_path, req)
|
1730
|
+
if 'data' not in seg]
|
1731
|
+
if not segments:
|
1732
|
+
# Degenerate case: just delete the manifest
|
1733
|
+
return self.app
|
1734
|
+
|
1735
|
+
segment_containers, segment_objects = zip(*(
|
1736
|
+
split_path(seg['name'], 2, 2, True) for seg in segments))
|
1737
|
+
segment_containers = set(segment_containers)
|
1738
|
+
if len(segment_containers) > 1:
|
1739
|
+
container_csv = ', '.join(
|
1740
|
+
'"%s"' % quote(c) for c in segment_containers)
|
1741
|
+
raise HTTPBadRequest('All segments must be in one container. '
|
1742
|
+
'Found segments in %s' % container_csv)
|
1743
|
+
if any(seg.get('sub_slo') for seg in segments):
|
1744
|
+
raise HTTPBadRequest('No segments may be large objects.')
|
1745
|
+
|
1746
|
+
# Auth checks
|
1747
|
+
segment_container = segment_containers.pop()
|
1748
|
+
if 'swift.authorize' in req.environ:
|
1749
|
+
container_info = get_container_info(
|
1750
|
+
req.environ, self.app, swift_source='SLO')
|
1751
|
+
req.acl = container_info.get('write_acl')
|
1752
|
+
aresp = req.environ['swift.authorize'](req)
|
1753
|
+
req.acl = None
|
1754
|
+
if aresp:
|
1755
|
+
return aresp
|
1756
|
+
|
1757
|
+
if bytes_to_wsgi(segment_container.encode('utf-8')) != container:
|
1758
|
+
path = '/%s/%s/%s' % (vrs, account, bytes_to_wsgi(
|
1759
|
+
segment_container.encode('utf-8')))
|
1760
|
+
seg_container_info = get_container_info(
|
1761
|
+
make_env(req.environ, path=path, swift_source='SLO'),
|
1762
|
+
self.app, swift_source='SLO')
|
1763
|
+
req.acl = seg_container_info.get('write_acl')
|
1764
|
+
aresp = req.environ['swift.authorize'](req)
|
1765
|
+
req.acl = None
|
1766
|
+
if aresp:
|
1767
|
+
return aresp
|
1768
|
+
|
1769
|
+
# Did our sanity checks; schedule segments to be deleted
|
1770
|
+
ts = req.ensure_x_timestamp()
|
1771
|
+
expirer_jobs = make_delete_jobs(
|
1772
|
+
wsgi_to_str(account), segment_container, segment_objects, ts)
|
1773
|
+
expiring_objects_account, expirer_cont = \
|
1774
|
+
self.expirer_config.get_expirer_account_and_container(
|
1775
|
+
ts, wsgi_to_str(account), wsgi_to_str(container),
|
1776
|
+
wsgi_to_str(obj))
|
1777
|
+
enqueue_req = make_pre_authed_request(
|
1778
|
+
req.environ,
|
1779
|
+
method='UPDATE',
|
1780
|
+
path="/v1/%s/%s" % (expiring_objects_account, expirer_cont),
|
1781
|
+
body=json.dumps(expirer_jobs),
|
1782
|
+
headers={'Content-Type': 'application/json',
|
1783
|
+
'X-Backend-Storage-Policy-Index': '0',
|
1784
|
+
'X-Backend-Allow-Private-Methods': 'True'},
|
1785
|
+
)
|
1786
|
+
resp = enqueue_req.get_response(self.app)
|
1787
|
+
if not resp.is_success:
|
1788
|
+
self.logger.error(
|
1789
|
+
'Failed to enqueue expiration entries: %s\n%s',
|
1790
|
+
resp.status, resp.body)
|
1791
|
+
return HTTPServiceUnavailable()
|
1792
|
+
# consume the response (should be short)
|
1793
|
+
friendly_close(resp)
|
1794
|
+
|
1795
|
+
# Finally, delete the manifest
|
1796
|
+
return self.app
|
1797
|
+
|
1499
1798
|
def handle_multipart_delete(self, req):
|
1500
1799
|
"""
|
1501
1800
|
Will delete all the segments in the SLO manifest and then, if
|
@@ -1504,6 +1803,10 @@ class StaticLargeObject(object):
|
|
1504
1803
|
:param req: a :class:`~swift.common.swob.Request` with an obj in path
|
1505
1804
|
:returns: swob.Response whose app_iter set to Bulk.handle_delete_iter
|
1506
1805
|
"""
|
1806
|
+
if self.allow_async_delete and config_true_value(
|
1807
|
+
req.params.get('async')):
|
1808
|
+
return self.handle_async_delete(req)
|
1809
|
+
|
1507
1810
|
req.headers['Content-Type'] = None # Ignore content-type from client
|
1508
1811
|
resp = HTTPOk(request=req)
|
1509
1812
|
try:
|
@@ -1594,6 +1897,8 @@ def filter_factory(global_conf, **local_conf):
|
|
1594
1897
|
DEFAULT_MAX_MANIFEST_SIZE))
|
1595
1898
|
yield_frequency = int(conf.get('yield_frequency',
|
1596
1899
|
DEFAULT_YIELD_FREQUENCY))
|
1900
|
+
allow_async_delete = config_true_value(conf.get('allow_async_delete',
|
1901
|
+
'true'))
|
1597
1902
|
|
1598
1903
|
register_swift_info('slo',
|
1599
1904
|
max_manifest_segments=max_manifest_segments,
|
@@ -1601,12 +1906,14 @@ def filter_factory(global_conf, **local_conf):
|
|
1601
1906
|
yield_frequency=yield_frequency,
|
1602
1907
|
# this used to be configurable; report it as 1 for
|
1603
1908
|
# clients that might still care
|
1604
|
-
min_segment_size=1
|
1909
|
+
min_segment_size=1,
|
1910
|
+
allow_async_delete=allow_async_delete)
|
1605
1911
|
|
1606
1912
|
def slo_filter(app):
|
1607
1913
|
return StaticLargeObject(
|
1608
1914
|
app, conf,
|
1609
1915
|
max_manifest_segments=max_manifest_segments,
|
1610
1916
|
max_manifest_size=max_manifest_size,
|
1611
|
-
yield_frequency=yield_frequency
|
1917
|
+
yield_frequency=yield_frequency,
|
1918
|
+
allow_async_delete=allow_async_delete)
|
1612
1919
|
return slo_filter
|