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
@@ -18,10 +18,44 @@
|
|
18
18
|
given account quota (in bytes) is exceeded while DELETE requests are still
|
19
19
|
allowed.
|
20
20
|
|
21
|
-
``account_quotas`` uses the
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
``account_quotas`` uses the following metadata entries to store the account
|
22
|
+
quota
|
23
|
+
|
24
|
+
+---------------------------------------------+-------------------------------+
|
25
|
+
|Metadata | Use |
|
26
|
+
+=============================================+===============================+
|
27
|
+
| X-Account-Meta-Quota-Bytes (obsoleted) | Maximum overall bytes stored |
|
28
|
+
| | in account across containers. |
|
29
|
+
+---------------------------------------------+-------------------------------+
|
30
|
+
| X-Account-Quota-Bytes | Maximum overall bytes stored |
|
31
|
+
| | in account across containers. |
|
32
|
+
+---------------------------------------------+-------------------------------+
|
33
|
+
| X-Account-Quota-Bytes-Policy-<policyname> | Maximum overall bytes stored |
|
34
|
+
| | in account across containers, |
|
35
|
+
| | for the given policy. |
|
36
|
+
+---------------------------------------------+-------------------------------+
|
37
|
+
| X-Account-Quota-Count | Maximum object count under |
|
38
|
+
| | account. |
|
39
|
+
+---------------------------------------------+-------------------------------+
|
40
|
+
| X-Account-Quota-Count-Policy-<policyname> | Maximum object count under |
|
41
|
+
| | account, for the given policy.|
|
42
|
+
+---------------------------------------------+-------------------------------+
|
43
|
+
|
44
|
+
|
45
|
+
Write requests to those metadata entries are only permitted for resellers.
|
46
|
+
There is no overall byte or object count limit set if the corresponding
|
47
|
+
metadata entries are not set.
|
48
|
+
|
49
|
+
Additionally, account quotas, of type quota-bytes or quota-count, may be set
|
50
|
+
for each storage policy, using metadata of the form ``x-account-<quota type>-\
|
51
|
+
policy-<policy name>``. Again, only resellers may update these metadata, and
|
52
|
+
there will be no limit for a particular policy if the corresponding metadata
|
53
|
+
is not set.
|
54
|
+
|
55
|
+
.. note::
|
56
|
+
Per-policy quotas need not sum to the overall account quota, and the sum of
|
57
|
+
all :ref:`container_quotas` for a given policy need not sum to the account's
|
58
|
+
policy quota.
|
25
59
|
|
26
60
|
The ``account_quotas`` middleware should be added to the pipeline in your
|
27
61
|
``/etc/swift/proxy-server.conf`` file just after any auth middleware.
|
@@ -54,8 +88,9 @@ account size has been updated.
|
|
54
88
|
|
55
89
|
from swift.common.swob import HTTPForbidden, HTTPBadRequest, \
|
56
90
|
HTTPRequestEntityTooLarge, wsgify
|
57
|
-
from swift.common.
|
58
|
-
from swift.
|
91
|
+
from swift.common.registry import register_swift_info
|
92
|
+
from swift.common.storage_policy import POLICIES
|
93
|
+
from swift.proxy.controllers.base import get_account_info, get_container_info
|
59
94
|
|
60
95
|
|
61
96
|
class AccountQuotaMiddleware(object):
|
@@ -67,12 +102,74 @@ class AccountQuotaMiddleware(object):
|
|
67
102
|
def __init__(self, app, *args, **kwargs):
|
68
103
|
self.app = app
|
69
104
|
|
105
|
+
def validate_and_translate_quotas(self, request, quota_type):
|
106
|
+
new_quotas = {}
|
107
|
+
new_quotas[None] = request.headers.get(
|
108
|
+
'X-Account-%s' % quota_type)
|
109
|
+
if request.headers.get(
|
110
|
+
'X-Remove-Account-%s' % quota_type):
|
111
|
+
new_quotas[None] = '' # X-Remove dominates if both are present
|
112
|
+
|
113
|
+
for policy in POLICIES:
|
114
|
+
tail = 'Account-%s-Policy-%s' % (quota_type, policy.name)
|
115
|
+
if request.headers.get('X-Remove-' + tail):
|
116
|
+
new_quotas[policy.idx] = ''
|
117
|
+
else:
|
118
|
+
quota = request.headers.pop('X-' + tail, None)
|
119
|
+
new_quotas[policy.idx] = quota
|
120
|
+
|
121
|
+
if request.environ.get('reseller_request') is True:
|
122
|
+
if any(quota and not quota.isdigit()
|
123
|
+
for quota in new_quotas.values()):
|
124
|
+
raise HTTPBadRequest()
|
125
|
+
for idx, quota in new_quotas.items():
|
126
|
+
if idx is None:
|
127
|
+
hdr = 'X-Account-Sysmeta-%s' % quota_type
|
128
|
+
else:
|
129
|
+
hdr = 'X-Account-Sysmeta-%s-Policy-%d' % (quota_type, idx)
|
130
|
+
request.headers[hdr] = quota
|
131
|
+
elif any(quota is not None for quota in new_quotas.values()):
|
132
|
+
# deny quota set for non-reseller
|
133
|
+
raise HTTPForbidden()
|
134
|
+
|
135
|
+
def handle_account(self, request):
|
136
|
+
if request.method in ("POST", "PUT"):
|
137
|
+
# Support old meta format
|
138
|
+
for legacy_header in [
|
139
|
+
'X-Account-Meta-Quota-Bytes',
|
140
|
+
'X-Remove-Account-Meta-Quota-Bytes',
|
141
|
+
]:
|
142
|
+
new_header = legacy_header.replace('-Meta-', '-')
|
143
|
+
legacy_value = request.headers.get(legacy_header)
|
144
|
+
if legacy_value is not None and not \
|
145
|
+
request.headers.get(new_header):
|
146
|
+
request.headers[new_header] = legacy_value
|
147
|
+
# account request, so we pay attention to the quotas
|
148
|
+
self.validate_and_translate_quotas(request, "Quota-Bytes")
|
149
|
+
self.validate_and_translate_quotas(request, "Quota-Count")
|
150
|
+
resp = request.get_response(self.app)
|
151
|
+
# Non-resellers can't update quotas, but they *can* see them
|
152
|
+
# Global quotas
|
153
|
+
postfixes = ('Quota-Bytes', 'Quota-Count')
|
154
|
+
for postfix in postfixes:
|
155
|
+
value = resp.headers.get('X-Account-Sysmeta-%s' % postfix)
|
156
|
+
if value:
|
157
|
+
resp.headers['X-Account-%s' % postfix] = value
|
158
|
+
|
159
|
+
# Per policy quotas
|
160
|
+
for policy in POLICIES:
|
161
|
+
infixes = ('Quota-Bytes-Policy', 'Quota-Count-Policy')
|
162
|
+
for infix in infixes:
|
163
|
+
value = resp.headers.get('X-Account-Sysmeta-%s-%d' % (
|
164
|
+
infix, policy.idx))
|
165
|
+
if value:
|
166
|
+
resp.headers['X-Account-%s-%s' % (
|
167
|
+
infix, policy.name)] = value
|
168
|
+
return resp
|
169
|
+
|
70
170
|
@wsgify
|
71
171
|
def __call__(self, request):
|
72
172
|
|
73
|
-
if request.method not in ("POST", "PUT"):
|
74
|
-
return self.app
|
75
|
-
|
76
173
|
try:
|
77
174
|
ver, account, container, obj = request.split_path(
|
78
175
|
2, 4, rest_with_last=True)
|
@@ -80,57 +177,125 @@ class AccountQuotaMiddleware(object):
|
|
80
177
|
return self.app
|
81
178
|
|
82
179
|
if not container:
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
remove_quota = request.headers.get(
|
87
|
-
'X-Remove-Account-Meta-Quota-Bytes')
|
88
|
-
else:
|
89
|
-
# container or object request; even if the quota headers are set
|
90
|
-
# in the request, they're meaningless
|
91
|
-
new_quota = remove_quota = None
|
92
|
-
|
93
|
-
if remove_quota:
|
94
|
-
new_quota = 0 # X-Remove dominates if both are present
|
180
|
+
return self.handle_account(request)
|
181
|
+
# container or object request; even if the quota headers are set
|
182
|
+
# in the request, they're meaningless
|
95
183
|
|
96
|
-
if request.
|
97
|
-
if new_quota and not new_quota.isdigit():
|
98
|
-
return HTTPBadRequest()
|
184
|
+
if not (request.method == "PUT" and obj):
|
99
185
|
return self.app
|
186
|
+
# OK, object PUT
|
100
187
|
|
101
|
-
|
102
|
-
|
103
|
-
return HTTPForbidden()
|
104
|
-
|
105
|
-
if request.method == "POST" or not obj:
|
188
|
+
if request.environ.get('reseller_request') is True:
|
189
|
+
# but resellers aren't constrained by quotas :-)
|
106
190
|
return self.app
|
107
191
|
|
192
|
+
# Object PUT request
|
108
193
|
content_length = (request.content_length or 0)
|
109
194
|
|
110
|
-
account_info = get_account_info(request.environ, self.app
|
111
|
-
|
195
|
+
account_info = get_account_info(request.environ, self.app,
|
196
|
+
swift_source='AQ')
|
197
|
+
if not account_info:
|
112
198
|
return self.app
|
199
|
+
|
200
|
+
# Check for quota byte violation
|
113
201
|
try:
|
114
|
-
quota = int(
|
202
|
+
quota = int(
|
203
|
+
account_info["sysmeta"].get(
|
204
|
+
"quota-bytes", account_info["meta"].get("quota-bytes", -1)
|
205
|
+
)
|
206
|
+
)
|
115
207
|
except ValueError:
|
208
|
+
quota = -1
|
209
|
+
if quota >= 0:
|
210
|
+
new_size = int(account_info['bytes']) + content_length
|
211
|
+
if quota < new_size:
|
212
|
+
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
|
213
|
+
if 'swift.authorize' in request.environ:
|
214
|
+
orig_authorize = request.environ['swift.authorize']
|
215
|
+
|
216
|
+
def reject_authorize(*args, **kwargs):
|
217
|
+
aresp = orig_authorize(*args, **kwargs)
|
218
|
+
if aresp:
|
219
|
+
return aresp
|
220
|
+
return resp
|
221
|
+
request.environ['swift.authorize'] = reject_authorize
|
222
|
+
else:
|
223
|
+
return resp
|
224
|
+
|
225
|
+
# Check for quota count violation
|
226
|
+
try:
|
227
|
+
quota = int(account_info['sysmeta'].get('quota-count', -1))
|
228
|
+
except ValueError:
|
229
|
+
quota = -1
|
230
|
+
if quota >= 0:
|
231
|
+
new_count = int(account_info['total_object_count']) + 1
|
232
|
+
if quota < new_count:
|
233
|
+
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
|
234
|
+
if 'swift.authorize' in request.environ:
|
235
|
+
orig_authorize = request.environ['swift.authorize']
|
236
|
+
|
237
|
+
def reject_authorize(*args, **kwargs):
|
238
|
+
aresp = orig_authorize(*args, **kwargs)
|
239
|
+
if aresp:
|
240
|
+
return aresp
|
241
|
+
return resp
|
242
|
+
request.environ['swift.authorize'] = reject_authorize
|
243
|
+
else:
|
244
|
+
return resp
|
245
|
+
|
246
|
+
container_info = get_container_info(request.environ, self.app,
|
247
|
+
swift_source='AQ')
|
248
|
+
if not container_info:
|
116
249
|
return self.app
|
117
|
-
|
118
|
-
return self.app
|
250
|
+
policy_idx = container_info['storage_policy']
|
119
251
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
252
|
+
# Check quota-byte per policy
|
253
|
+
sysmeta_key = 'quota-bytes-policy-%s' % policy_idx
|
254
|
+
try:
|
255
|
+
policy_quota = int(account_info['sysmeta'].get(sysmeta_key, -1))
|
256
|
+
except ValueError:
|
257
|
+
policy_quota = -1
|
258
|
+
if policy_quota >= 0:
|
259
|
+
policy_stats = account_info['storage_policies'].get(policy_idx, {})
|
260
|
+
new_size = int(policy_stats.get('bytes', 0)) + content_length
|
261
|
+
if policy_quota < new_size:
|
262
|
+
resp = HTTPRequestEntityTooLarge(
|
263
|
+
body='Upload exceeds policy quota.')
|
264
|
+
if 'swift.authorize' in request.environ:
|
265
|
+
orig_authorize = request.environ['swift.authorize']
|
266
|
+
|
267
|
+
def reject_authorize(*args, **kwargs):
|
268
|
+
aresp = orig_authorize(*args, **kwargs)
|
269
|
+
if aresp:
|
270
|
+
return aresp
|
271
|
+
return resp
|
272
|
+
request.environ['swift.authorize'] = reject_authorize
|
273
|
+
else:
|
274
|
+
return resp
|
125
275
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
276
|
+
# Check quota-count per policy
|
277
|
+
sysmeta_key = 'quota-count-policy-%s' % policy_idx
|
278
|
+
try:
|
279
|
+
policy_quota = int(account_info['sysmeta'].get(sysmeta_key, -1))
|
280
|
+
except ValueError:
|
281
|
+
policy_quota = -1
|
282
|
+
if policy_quota >= 0:
|
283
|
+
policy_stats = account_info['storage_policies'].get(policy_idx, {})
|
284
|
+
new_size = int(policy_stats.get('object_count', 0)) + 1
|
285
|
+
if policy_quota < new_size:
|
286
|
+
resp = HTTPRequestEntityTooLarge(
|
287
|
+
body='Upload exceeds policy quota.')
|
288
|
+
if 'swift.authorize' in request.environ:
|
289
|
+
orig_authorize = request.environ['swift.authorize']
|
290
|
+
|
291
|
+
def reject_authorize(*args, **kwargs):
|
292
|
+
aresp = orig_authorize(*args, **kwargs)
|
293
|
+
if aresp:
|
294
|
+
return aresp
|
295
|
+
return resp
|
296
|
+
request.environ['swift.authorize'] = reject_authorize
|
297
|
+
else:
|
130
298
|
return resp
|
131
|
-
request.environ['swift.authorize'] = reject_authorize
|
132
|
-
else:
|
133
|
-
return resp
|
134
299
|
|
135
300
|
return self.app
|
136
301
|
|
swift/common/middleware/acl.py
CHANGED
@@ -14,8 +14,7 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
16
|
import json
|
17
|
-
import
|
18
|
-
from six.moves.urllib.parse import unquote, urlparse
|
17
|
+
from urllib.parse import unquote, urlparse
|
19
18
|
|
20
19
|
|
21
20
|
def clean_acl(name, value):
|
@@ -217,7 +216,7 @@ def parse_acl_v2(data):
|
|
217
216
|
"""
|
218
217
|
if data is None:
|
219
218
|
return None
|
220
|
-
if data
|
219
|
+
if data == '':
|
221
220
|
return {}
|
222
221
|
try:
|
223
222
|
result = json.loads(data)
|
@@ -294,12 +293,8 @@ def acls_from_account_info(info):
|
|
294
293
|
if not any((admin_members, readwrite_members, readonly_members)):
|
295
294
|
return None
|
296
295
|
|
297
|
-
|
296
|
+
return {
|
298
297
|
'admin': admin_members,
|
299
298
|
'read-write': readwrite_members,
|
300
299
|
'read-only': readonly_members,
|
301
300
|
}
|
302
|
-
if six.PY2:
|
303
|
-
for k in ('admin', 'read-write', 'read-only'):
|
304
|
-
acls[k] = [v.encode('utf8') for v in acls[k]]
|
305
|
-
return acls
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# Copyright (c) 2022 NVIDIA
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
12
|
+
# implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
import os
|
16
|
+
import time
|
17
|
+
|
18
|
+
from swift.common.request_helpers import split_and_validate_path
|
19
|
+
from swift.common.swob import Request, HTTPTooManyBackendRequests, \
|
20
|
+
HTTPException
|
21
|
+
from swift.common.utils import get_logger, non_negative_float, \
|
22
|
+
EventletRateLimiter, readconf
|
23
|
+
|
24
|
+
RATE_LIMITED_METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'UPDATE',
|
25
|
+
'REPLICATE')
|
26
|
+
BACKEND_RATELIMIT_CONFIG_SECTION = 'backend_ratelimit'
|
27
|
+
DEFAULT_BACKEND_RATELIMIT_CONF_FILE = 'backend-ratelimit.conf'
|
28
|
+
DEFAULT_CONFIG_RELOAD_INTERVAL = 60.0
|
29
|
+
DEFAULT_REQUESTS_PER_DEVICE_PER_SECOND = 0.0
|
30
|
+
DEFAULT_REQUESTS_PER_DEVICE_RATE_BUFFER = 1.0
|
31
|
+
|
32
|
+
|
33
|
+
class BackendRateLimitMiddleware(object):
|
34
|
+
"""
|
35
|
+
Backend rate-limiting middleware.
|
36
|
+
|
37
|
+
Rate-limits requests to backend storage node devices. Each (device, request
|
38
|
+
method) combination is independently rate-limited. All requests with a
|
39
|
+
'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'UPDATE' or 'REPLICATE' method are
|
40
|
+
rate limited on a per-device basis by both a method-specific rate and an
|
41
|
+
overall device rate limit.
|
42
|
+
|
43
|
+
If a request would cause the rate-limit to be exceeded for the method
|
44
|
+
and/or device then a response with a 529 status code is returned.
|
45
|
+
"""
|
46
|
+
def __init__(self, app, filter_conf, logger=None):
|
47
|
+
self.app = app
|
48
|
+
self.filter_conf = filter_conf
|
49
|
+
self.logger = logger or get_logger(self.filter_conf,
|
50
|
+
log_route='backend_ratelimit')
|
51
|
+
self.requests_per_device_rate_buffer = \
|
52
|
+
DEFAULT_REQUESTS_PER_DEVICE_RATE_BUFFER
|
53
|
+
# map (device, method) -> rate
|
54
|
+
self.requests_per_device_per_second = {}
|
55
|
+
# map (device, method) -> RateLimiter, populated on-demand
|
56
|
+
self.rate_limiters = {}
|
57
|
+
|
58
|
+
# some config options are *only* read from filter conf at startup...
|
59
|
+
default_conf_path = os.path.join(
|
60
|
+
self.filter_conf.get('swift_dir', '/etc/swift'),
|
61
|
+
DEFAULT_BACKEND_RATELIMIT_CONF_FILE)
|
62
|
+
try:
|
63
|
+
self.conf_path = self.filter_conf['backend_ratelimit_conf_path']
|
64
|
+
self.is_config_file_expected = True
|
65
|
+
except KeyError:
|
66
|
+
self.conf_path = default_conf_path
|
67
|
+
self.is_config_file_expected = False
|
68
|
+
self.config_reload_interval = non_negative_float(
|
69
|
+
filter_conf.get('config_reload_interval',
|
70
|
+
DEFAULT_CONFIG_RELOAD_INTERVAL))
|
71
|
+
|
72
|
+
# other conf options are read from filter section at startup but may
|
73
|
+
# also be overridden by options in a separate config file...
|
74
|
+
self._last_config_reload_attempt = time.time()
|
75
|
+
self._apply_config(self.filter_conf)
|
76
|
+
self._load_config_file()
|
77
|
+
|
78
|
+
def _refresh_ratelimiters(self):
|
79
|
+
# note: if we ever wanted to prune the ratelimiters (in case devices
|
80
|
+
# have been removed) we could inspect each ratelimiter's running_time
|
81
|
+
# and remove those with very old running_time
|
82
|
+
for (dev, method), rl in self.rate_limiters.items():
|
83
|
+
rl.set_max_rate(self.requests_per_device_per_second[method])
|
84
|
+
rl.set_rate_buffer(self.requests_per_device_rate_buffer)
|
85
|
+
|
86
|
+
def _apply_config(self, conf):
|
87
|
+
modified = False
|
88
|
+
reqs_per_device_rate_buffer = non_negative_float(
|
89
|
+
conf.get('requests_per_device_rate_buffer',
|
90
|
+
DEFAULT_REQUESTS_PER_DEVICE_RATE_BUFFER))
|
91
|
+
|
92
|
+
# note: 'None' key holds the aggregate per-device limit for all methods
|
93
|
+
reqs_per_device_per_second = {None: non_negative_float(
|
94
|
+
conf.get('requests_per_device_per_second', 0.0))}
|
95
|
+
for method in RATE_LIMITED_METHODS:
|
96
|
+
val = non_negative_float(
|
97
|
+
conf.get('%s_requests_per_device_per_second'
|
98
|
+
% method.lower(), 0.0))
|
99
|
+
reqs_per_device_per_second[method] = val
|
100
|
+
|
101
|
+
if reqs_per_device_rate_buffer != self.requests_per_device_rate_buffer:
|
102
|
+
self.requests_per_device_rate_buffer = reqs_per_device_rate_buffer
|
103
|
+
modified = True
|
104
|
+
if reqs_per_device_per_second != self.requests_per_device_per_second:
|
105
|
+
self.requests_per_device_per_second = reqs_per_device_per_second
|
106
|
+
self.is_any_rate_limit_configured = any(
|
107
|
+
self.requests_per_device_per_second.values())
|
108
|
+
modified = True
|
109
|
+
if modified:
|
110
|
+
self._refresh_ratelimiters()
|
111
|
+
return modified
|
112
|
+
|
113
|
+
def _load_config_file(self):
|
114
|
+
# If conf file can be read then apply its options to the filter conf
|
115
|
+
# options, discarding *all* options previously loaded from the conf
|
116
|
+
# file i.e. options deleted from the conf file will revert to the
|
117
|
+
# filter conf value or default value. If the conf file cannot be read
|
118
|
+
# or is invalid, then the current config is left unchanged.
|
119
|
+
try:
|
120
|
+
new_conf = dict(self.filter_conf) # filter_conf not current conf
|
121
|
+
new_conf.update(
|
122
|
+
readconf(self.conf_path, BACKEND_RATELIMIT_CONFIG_SECTION))
|
123
|
+
modified = self._apply_config(new_conf)
|
124
|
+
if modified:
|
125
|
+
self.logger.info('Loaded config file %s, config changed',
|
126
|
+
self.conf_path)
|
127
|
+
elif not self.is_config_file_expected:
|
128
|
+
self.logger.info('Loaded new config file %s, config unchanged',
|
129
|
+
self.conf_path)
|
130
|
+
else:
|
131
|
+
self.logger.debug(
|
132
|
+
'Loaded existing config file %s, config unchanged',
|
133
|
+
self.conf_path)
|
134
|
+
self.is_config_file_expected = True
|
135
|
+
except IOError as err:
|
136
|
+
if self.is_config_file_expected:
|
137
|
+
self.logger.warning(
|
138
|
+
'Failed to load config file, config unchanged: %s', err)
|
139
|
+
self.is_config_file_expected = False
|
140
|
+
except ValueError as err:
|
141
|
+
# ...but if it exists it should be valid
|
142
|
+
self.logger.warning('Invalid config file %s, config unchanged: %s',
|
143
|
+
self.conf_path, err)
|
144
|
+
|
145
|
+
def _maybe_reload_config(self):
|
146
|
+
if self.config_reload_interval:
|
147
|
+
now = time.time()
|
148
|
+
if (now - self._last_config_reload_attempt
|
149
|
+
>= self.config_reload_interval):
|
150
|
+
try:
|
151
|
+
self._load_config_file()
|
152
|
+
except Exception: # noqa
|
153
|
+
self.logger.exception('Error reloading config file')
|
154
|
+
finally:
|
155
|
+
# always reset last loaded time to avoid re-try storm
|
156
|
+
self._last_config_reload_attempt = now
|
157
|
+
|
158
|
+
def _get_ratelimiter(self, device, method=None):
|
159
|
+
"""
|
160
|
+
Get a rate limiter for the (device, method) combination. If a rate
|
161
|
+
limiter does not yet exist for the given (device, method) combination
|
162
|
+
then it is created and added to the map of rate limiters.
|
163
|
+
|
164
|
+
:param: the device.
|
165
|
+
:method: the request method; if None then the aggregate rate limiter
|
166
|
+
for all requests to the device is returned.
|
167
|
+
:returns: an instance of ``EventletRateLimiter``.
|
168
|
+
"""
|
169
|
+
try:
|
170
|
+
rl = self.rate_limiters[(device, method)]
|
171
|
+
except KeyError:
|
172
|
+
rl = EventletRateLimiter(
|
173
|
+
max_rate=self.requests_per_device_per_second[method],
|
174
|
+
rate_buffer=self.requests_per_device_rate_buffer,
|
175
|
+
running_time=time.time(),
|
176
|
+
burst_after_idle=True)
|
177
|
+
self.rate_limiters[(device, method)] = rl
|
178
|
+
return rl
|
179
|
+
|
180
|
+
def _is_allowed(self, device, method):
|
181
|
+
"""
|
182
|
+
Evaluate backend rate-limiting policies for the incoming request.
|
183
|
+
|
184
|
+
A request is allowed when neither the per-(device, method) rate-limit
|
185
|
+
nor the per-device rate-limit has been reached.
|
186
|
+
|
187
|
+
Note: a request will be disallowed if the aggregate per-device
|
188
|
+
rate-limit has been reached, even if the per-(device, method)
|
189
|
+
rate-limit has not been reached for the request's method.
|
190
|
+
|
191
|
+
:param: the device.
|
192
|
+
:method: the request method.
|
193
|
+
:returns: boolean, is_allowed.
|
194
|
+
"""
|
195
|
+
return (self._get_ratelimiter(device, None).is_allowed()
|
196
|
+
and self._get_ratelimiter(device, method).is_allowed())
|
197
|
+
|
198
|
+
def __call__(self, env, start_response):
|
199
|
+
"""
|
200
|
+
WSGI entry point.
|
201
|
+
|
202
|
+
:param env: WSGI environment dictionary
|
203
|
+
:param start_response: WSGI callable
|
204
|
+
"""
|
205
|
+
self._maybe_reload_config()
|
206
|
+
req = Request(env)
|
207
|
+
handler = self.app
|
208
|
+
if (self.is_any_rate_limit_configured
|
209
|
+
and req.method in RATE_LIMITED_METHODS):
|
210
|
+
try:
|
211
|
+
device, partition, _ = split_and_validate_path(req, 1, 3, True)
|
212
|
+
int(partition) # check it's a valid partition
|
213
|
+
except (ValueError, HTTPException):
|
214
|
+
# request may not have device/partition e.g. a healthcheck req
|
215
|
+
pass
|
216
|
+
else:
|
217
|
+
if not self._is_allowed(device, req.method):
|
218
|
+
self.logger.increment('backend.ratelimit')
|
219
|
+
handler = HTTPTooManyBackendRequests()
|
220
|
+
return handler(env, start_response)
|
221
|
+
|
222
|
+
|
223
|
+
def filter_factory(global_conf, **local_conf):
|
224
|
+
conf = global_conf.copy()
|
225
|
+
conf.update(local_conf)
|
226
|
+
|
227
|
+
def backend_ratelimit_filter(app):
|
228
|
+
return BackendRateLimitMiddleware(app, conf)
|
229
|
+
|
230
|
+
return backend_ratelimit_filter
|