swift 2.32.0__py2.py3-none-any.whl → 2.34.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swift/account/auditor.py +11 -0
- swift/account/reaper.py +11 -1
- swift/account/replicator.py +22 -0
- swift/account/server.py +13 -12
- swift-2.32.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
- swift-2.32.0.data/scripts/swift-config → swift/cli/config.py +1 -1
- swift-2.32.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
- swift-2.32.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
- swift-2.32.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
- swift/cli/info.py +131 -3
- swift-2.32.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
- swift-2.32.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
- swift-2.32.0.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +9 -18
- swift-2.32.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +1 -1
- swift/cli/reload.py +141 -0
- swift/cli/ringbuilder.py +24 -0
- swift/common/daemon.py +12 -2
- swift/common/db.py +14 -9
- swift/common/db_auditor.py +2 -2
- swift/common/db_replicator.py +6 -0
- swift/common/exceptions.py +12 -0
- swift/common/http_protocol.py +76 -3
- swift/common/manager.py +120 -5
- swift/common/memcached.py +24 -25
- swift/common/middleware/account_quotas.py +144 -43
- swift/common/middleware/backend_ratelimit.py +166 -24
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +3 -5
- swift/common/middleware/container_sync.py +6 -10
- swift/common/middleware/crypto/crypto_utils.py +4 -5
- swift/common/middleware/crypto/decrypter.py +4 -5
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/proxy_logging.py +57 -43
- swift/common/middleware/ratelimit.py +6 -7
- swift/common/middleware/recon.py +6 -7
- swift/common/middleware/s3api/acl_handlers.py +10 -1
- swift/common/middleware/s3api/controllers/__init__.py +3 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_upload.py +31 -15
- swift/common/middleware/s3api/controllers/obj.py +20 -1
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/s3api.py +6 -0
- swift/common/middleware/s3api/s3request.py +190 -74
- swift/common/middleware/s3api/s3response.py +48 -8
- swift/common/middleware/s3api/s3token.py +2 -2
- swift/common/middleware/s3api/utils.py +2 -1
- swift/common/middleware/slo.py +508 -310
- swift/common/middleware/staticweb.py +45 -14
- swift/common/middleware/tempauth.py +6 -4
- swift/common/middleware/tempurl.py +134 -93
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +9 -10
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +1 -2
- swift/common/request_helpers.py +101 -8
- swift/common/statsd_client.py +207 -0
- swift/common/storage_policy.py +1 -1
- swift/common/swob.py +5 -2
- swift/common/utils/__init__.py +331 -1774
- swift/common/utils/base.py +138 -0
- swift/common/utils/config.py +443 -0
- swift/common/utils/logs.py +999 -0
- swift/common/utils/timestamp.py +23 -2
- swift/common/wsgi.py +19 -3
- swift/container/auditor.py +11 -0
- swift/container/backend.py +136 -31
- swift/container/reconciler.py +11 -2
- swift/container/replicator.py +64 -7
- swift/container/server.py +276 -146
- swift/container/sharder.py +86 -42
- swift/container/sync.py +11 -1
- swift/container/updater.py +12 -2
- swift/obj/auditor.py +20 -3
- swift/obj/diskfile.py +63 -25
- swift/obj/expirer.py +154 -47
- swift/obj/mem_diskfile.py +2 -1
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +28 -4
- swift/obj/replicator.py +63 -24
- swift/obj/server.py +76 -59
- swift/obj/updater.py +12 -2
- swift/obj/watchers/dark_data.py +72 -34
- swift/proxy/controllers/account.py +3 -2
- swift/proxy/controllers/base.py +254 -148
- swift/proxy/controllers/container.py +274 -289
- swift/proxy/controllers/obj.py +120 -166
- swift/proxy/server.py +17 -13
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +14 -4
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/METADATA +9 -7
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/RECORD +97 -120
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +39 -0
- swift-2.34.0.dist-info/pbr.json +1 -0
- swift-2.32.0.data/scripts/swift-account-auditor +0 -23
- swift-2.32.0.data/scripts/swift-account-info +0 -52
- swift-2.32.0.data/scripts/swift-account-reaper +0 -23
- swift-2.32.0.data/scripts/swift-account-replicator +0 -34
- swift-2.32.0.data/scripts/swift-account-server +0 -23
- swift-2.32.0.data/scripts/swift-container-auditor +0 -23
- swift-2.32.0.data/scripts/swift-container-info +0 -56
- swift-2.32.0.data/scripts/swift-container-reconciler +0 -21
- swift-2.32.0.data/scripts/swift-container-replicator +0 -34
- swift-2.32.0.data/scripts/swift-container-server +0 -23
- swift-2.32.0.data/scripts/swift-container-sharder +0 -37
- swift-2.32.0.data/scripts/swift-container-sync +0 -23
- swift-2.32.0.data/scripts/swift-container-updater +0 -23
- swift-2.32.0.data/scripts/swift-dispersion-report +0 -24
- swift-2.32.0.data/scripts/swift-form-signature +0 -20
- swift-2.32.0.data/scripts/swift-init +0 -119
- swift-2.32.0.data/scripts/swift-object-auditor +0 -29
- swift-2.32.0.data/scripts/swift-object-expirer +0 -33
- swift-2.32.0.data/scripts/swift-object-info +0 -60
- swift-2.32.0.data/scripts/swift-object-reconstructor +0 -33
- swift-2.32.0.data/scripts/swift-object-relinker +0 -23
- swift-2.32.0.data/scripts/swift-object-replicator +0 -37
- swift-2.32.0.data/scripts/swift-object-server +0 -27
- swift-2.32.0.data/scripts/swift-object-updater +0 -23
- swift-2.32.0.data/scripts/swift-proxy-server +0 -23
- swift-2.32.0.data/scripts/swift-recon +0 -24
- swift-2.32.0.data/scripts/swift-ring-builder +0 -37
- swift-2.32.0.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.32.0.data/scripts/swift-ring-composer +0 -22
- swift-2.32.0.dist-info/pbr.json +0 -1
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
swift/common/memcached.py
CHANGED
@@ -59,6 +59,8 @@ from eventlet import Timeout
|
|
59
59
|
from six.moves import range
|
60
60
|
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
|
61
61
|
from swift.common import utils
|
62
|
+
from swift.common.exceptions import MemcacheConnectionError, \
|
63
|
+
MemcacheIncrNotFoundError, MemcachePoolTimeout
|
62
64
|
from swift.common.utils import md5, human_readable, config_true_value, \
|
63
65
|
memcached_timing_stats
|
64
66
|
|
@@ -83,6 +85,9 @@ TIMING_SAMPLE_RATE_HIGH = 0.1
|
|
83
85
|
TIMING_SAMPLE_RATE_MEDIUM = 0.01
|
84
86
|
TIMING_SAMPLE_RATE_LOW = 0.001
|
85
87
|
|
88
|
+
# The max value of a delta expiration time.
|
89
|
+
EXPTIME_MAXDELTA = 30 * 24 * 60 * 60
|
90
|
+
|
86
91
|
|
87
92
|
def md5hash(key):
|
88
93
|
if not isinstance(key, bytes):
|
@@ -100,7 +105,7 @@ def sanitize_timeout(timeout):
|
|
100
105
|
translates negative values to mean a delta of 30 days in seconds (and 1
|
101
106
|
additional second), client beware.
|
102
107
|
"""
|
103
|
-
if timeout >
|
108
|
+
if timeout > EXPTIME_MAXDELTA:
|
104
109
|
timeout += tm.time()
|
105
110
|
return int(timeout)
|
106
111
|
|
@@ -119,18 +124,6 @@ def set_msg(key, flags, timeout, value):
|
|
119
124
|
]) + (b'\r\n' + value + b'\r\n')
|
120
125
|
|
121
126
|
|
122
|
-
class MemcacheConnectionError(Exception):
|
123
|
-
pass
|
124
|
-
|
125
|
-
|
126
|
-
class MemcacheIncrNotFoundError(MemcacheConnectionError):
|
127
|
-
pass
|
128
|
-
|
129
|
-
|
130
|
-
class MemcachePoolTimeout(Timeout):
|
131
|
-
pass
|
132
|
-
|
133
|
-
|
134
127
|
class MemcacheConnPool(Pool):
|
135
128
|
"""
|
136
129
|
Connection pool for Memcache Connections
|
@@ -245,6 +238,13 @@ class MemcacheRing(object):
|
|
245
238
|
def memcache_servers(self):
|
246
239
|
return list(self._client_cache.keys())
|
247
240
|
|
241
|
+
def _log_error(self, server, cmd, action, msg):
|
242
|
+
self.logger.error(
|
243
|
+
"Error %(action)s to memcached: %(server)s"
|
244
|
+
": with key_prefix %(key_prefix)s, method %(method)s: %(msg)s",
|
245
|
+
{'action': action, 'server': server, 'key_prefix': cmd.key_prefix,
|
246
|
+
'method': cmd.method, 'msg': msg})
|
247
|
+
|
248
248
|
"""
|
249
249
|
Handles exceptions.
|
250
250
|
|
@@ -367,7 +367,8 @@ class MemcacheRing(object):
|
|
367
367
|
self._exception_occurred(server, e, cmd, pool_start_time,
|
368
368
|
action='connecting', sock=sock)
|
369
369
|
if not any_yielded:
|
370
|
-
self.
|
370
|
+
self._log_error('ALL', cmd, 'connecting',
|
371
|
+
'No more memcached servers to try')
|
371
372
|
|
372
373
|
def _return_conn(self, server, fp, sock):
|
373
374
|
"""Returns a server connection to the pool."""
|
@@ -404,6 +405,14 @@ class MemcacheRing(object):
|
|
404
405
|
elif not isinstance(value, bytes):
|
405
406
|
value = str(value).encode('utf-8')
|
406
407
|
|
408
|
+
if 0 <= self.item_size_warning_threshold <= len(value):
|
409
|
+
self.logger.warning(
|
410
|
+
"Item size larger than warning threshold: "
|
411
|
+
"%d (%s) >= %d (%s)", len(value),
|
412
|
+
human_readable(len(value)),
|
413
|
+
self.item_size_warning_threshold,
|
414
|
+
human_readable(self.item_size_warning_threshold))
|
415
|
+
|
407
416
|
for (server, fp, sock) in self._get_conns(cmd):
|
408
417
|
conn_start_time = tm.time()
|
409
418
|
try:
|
@@ -414,17 +423,7 @@ class MemcacheRing(object):
|
|
414
423
|
if msg != b'STORED':
|
415
424
|
if not six.PY2:
|
416
425
|
msg = msg.decode('ascii')
|
417
|
-
|
418
|
-
"Error setting value in memcached: "
|
419
|
-
"%(server)s: %(msg)s",
|
420
|
-
{'server': server, 'msg': msg})
|
421
|
-
if 0 <= self.item_size_warning_threshold <= len(value):
|
422
|
-
self.logger.warning(
|
423
|
-
"Item size larger than warning threshold: "
|
424
|
-
"%d (%s) >= %d (%s)", len(value),
|
425
|
-
human_readable(len(value)),
|
426
|
-
self.item_size_warning_threshold,
|
427
|
-
human_readable(self.item_size_warning_threshold))
|
426
|
+
raise MemcacheConnectionError('failed set: %s' % msg)
|
428
427
|
self._return_conn(server, fp, sock)
|
429
428
|
return
|
430
429
|
except (Exception, Timeout) as e:
|
@@ -18,15 +18,39 @@
|
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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.
|
30
54
|
|
31
55
|
.. note::
|
32
56
|
Per-policy quotas need not sum to the overall account quota, and the sum of
|
@@ -78,45 +102,69 @@ class AccountQuotaMiddleware(object):
|
|
78
102
|
def __init__(self, app, *args, **kwargs):
|
79
103
|
self.app = app
|
80
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
|
+
|
81
135
|
def handle_account(self, request):
|
82
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
|
83
147
|
# account request, so we pay attention to the quotas
|
84
|
-
|
85
|
-
|
86
|
-
'X-Account-Meta-Quota-Bytes')
|
87
|
-
if request.headers.get(
|
88
|
-
'X-Remove-Account-Meta-Quota-Bytes'):
|
89
|
-
new_quotas[None] = 0 # X-Remove dominates if both are present
|
90
|
-
|
91
|
-
for policy in POLICIES:
|
92
|
-
tail = 'Account-Quota-Bytes-Policy-%s' % policy.name
|
93
|
-
if request.headers.get('X-Remove-' + tail):
|
94
|
-
new_quotas[policy.idx] = 0
|
95
|
-
else:
|
96
|
-
quota = request.headers.pop('X-' + tail, None)
|
97
|
-
new_quotas[policy.idx] = quota
|
98
|
-
|
99
|
-
if request.environ.get('reseller_request') is True:
|
100
|
-
if any(quota and not quota.isdigit()
|
101
|
-
for quota in new_quotas.values()):
|
102
|
-
return HTTPBadRequest()
|
103
|
-
for idx, quota in new_quotas.items():
|
104
|
-
if idx is None:
|
105
|
-
continue # For legacy reasons, it's in user meta
|
106
|
-
hdr = 'X-Account-Sysmeta-Quota-Bytes-Policy-%d' % idx
|
107
|
-
request.headers[hdr] = quota
|
108
|
-
elif any(quota is not None for quota in new_quotas.values()):
|
109
|
-
# deny quota set for non-reseller
|
110
|
-
return HTTPForbidden()
|
111
|
-
|
148
|
+
self.validate_and_translate_quotas(request, "Quota-Bytes")
|
149
|
+
self.validate_and_translate_quotas(request, "Quota-Count")
|
112
150
|
resp = request.get_response(self.app)
|
113
151
|
# Non-resellers can't update quotas, but they *can* see them
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
152
|
+
# Global quotas
|
153
|
+
postfixes = ('Quota-Bytes', 'Quota-Count')
|
154
|
+
for postfix in postfixes:
|
155
|
+
value = resp.headers.get('X-Account-Sysmeta-%s' % postfix)
|
118
156
|
if value:
|
119
|
-
resp.headers['X-Account-%s
|
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
|
120
168
|
return resp
|
121
169
|
|
122
170
|
@wsgify
|
@@ -148,8 +196,14 @@ class AccountQuotaMiddleware(object):
|
|
148
196
|
swift_source='AQ')
|
149
197
|
if not account_info:
|
150
198
|
return self.app
|
199
|
+
|
200
|
+
# Check for quota byte violation
|
151
201
|
try:
|
152
|
-
quota = int(
|
202
|
+
quota = int(
|
203
|
+
account_info["sysmeta"].get(
|
204
|
+
"quota-bytes", account_info["meta"].get("quota-bytes", -1)
|
205
|
+
)
|
206
|
+
)
|
153
207
|
except ValueError:
|
154
208
|
quota = -1
|
155
209
|
if quota >= 0:
|
@@ -168,11 +222,34 @@ class AccountQuotaMiddleware(object):
|
|
168
222
|
else:
|
169
223
|
return resp
|
170
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
|
+
|
171
246
|
container_info = get_container_info(request.environ, self.app,
|
172
247
|
swift_source='AQ')
|
173
248
|
if not container_info:
|
174
249
|
return self.app
|
175
250
|
policy_idx = container_info['storage_policy']
|
251
|
+
|
252
|
+
# Check quota-byte per policy
|
176
253
|
sysmeta_key = 'quota-bytes-policy-%s' % policy_idx
|
177
254
|
try:
|
178
255
|
policy_quota = int(account_info['sysmeta'].get(sysmeta_key, -1))
|
@@ -196,6 +273,30 @@ class AccountQuotaMiddleware(object):
|
|
196
273
|
else:
|
197
274
|
return resp
|
198
275
|
|
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:
|
298
|
+
return resp
|
299
|
+
|
199
300
|
return self.app
|
200
301
|
|
201
302
|
|
@@ -12,47 +12,188 @@
|
|
12
12
|
# implied.
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
|
-
|
15
|
+
import os
|
16
16
|
import time
|
17
|
-
from collections import defaultdict
|
18
17
|
|
19
18
|
from swift.common.request_helpers import split_and_validate_path
|
20
19
|
from swift.common.swob import Request, HTTPTooManyBackendRequests, \
|
21
20
|
HTTPException
|
22
21
|
from swift.common.utils import get_logger, non_negative_float, \
|
23
|
-
EventletRateLimiter
|
22
|
+
EventletRateLimiter, readconf
|
24
23
|
|
25
24
|
RATE_LIMITED_METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'UPDATE',
|
26
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
|
27
31
|
|
28
32
|
|
29
33
|
class BackendRateLimitMiddleware(object):
|
30
34
|
"""
|
31
35
|
Backend rate-limiting middleware.
|
32
36
|
|
33
|
-
Rate-limits requests to backend storage node devices. Each device
|
34
|
-
independently rate-limited. All requests with a
|
35
|
-
'POST', 'DELETE', 'UPDATE' or 'REPLICATE' method are
|
36
|
-
rate
|
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.
|
37
42
|
|
38
|
-
If a request would cause the rate-limit to be exceeded
|
39
|
-
a 529 status code is returned.
|
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.
|
40
45
|
"""
|
41
|
-
def __init__(self, app,
|
46
|
+
def __init__(self, app, filter_conf, logger=None):
|
42
47
|
self.app = app
|
43
|
-
self.
|
44
|
-
self.
|
45
|
-
|
46
|
-
self.requests_per_device_rate_buffer =
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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],
|
53
174
|
rate_buffer=self.requests_per_device_rate_buffer,
|
54
175
|
running_time=time.time(),
|
55
|
-
burst_after_idle=True)
|
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())
|
56
197
|
|
57
198
|
def __call__(self, env, start_response):
|
58
199
|
"""
|
@@ -61,9 +202,11 @@ class BackendRateLimitMiddleware(object):
|
|
61
202
|
:param env: WSGI environment dictionary
|
62
203
|
:param start_response: WSGI callable
|
63
204
|
"""
|
205
|
+
self._maybe_reload_config()
|
64
206
|
req = Request(env)
|
65
207
|
handler = self.app
|
66
|
-
if
|
208
|
+
if (self.is_any_rate_limit_configured
|
209
|
+
and req.method in RATE_LIMITED_METHODS):
|
67
210
|
try:
|
68
211
|
device, partition, _ = split_and_validate_path(req, 1, 3, True)
|
69
212
|
int(partition) # check it's a valid partition
|
@@ -71,8 +214,7 @@ class BackendRateLimitMiddleware(object):
|
|
71
214
|
# request may not have device/partition e.g. a healthcheck req
|
72
215
|
pass
|
73
216
|
else:
|
74
|
-
|
75
|
-
if not rate_limiter.is_allowed():
|
217
|
+
if not self._is_allowed(device, req.method):
|
76
218
|
self.logger.increment('backend.ratelimit')
|
77
219
|
handler = HTTPTooManyBackendRequests()
|
78
220
|
return handler(env, start_response)
|
@@ -13,8 +13,6 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
-
from swift import gettext_ as _
|
17
|
-
|
18
16
|
from swift.common.swob import Request, HTTPServerError
|
19
17
|
from swift.common.utils import get_logger, generate_trans_id, close_if_possible
|
20
18
|
from swift.common.wsgi import WSGIContext
|
@@ -74,7 +72,7 @@ class CatchErrorsContext(WSGIContext):
|
|
74
72
|
# catch any errors in the pipeline
|
75
73
|
resp = self._app_call(env)
|
76
74
|
except: # noqa
|
77
|
-
self.logger.exception(
|
75
|
+
self.logger.exception('Error: An error occurred')
|
78
76
|
resp = HTTPServerError(request=Request(env),
|
79
77
|
body=b'An error occurred',
|
80
78
|
content_type='text/plain')
|
@@ -29,8 +29,6 @@ rewritten and the request is passed further down the WSGI chain.
|
|
29
29
|
|
30
30
|
import six
|
31
31
|
|
32
|
-
from swift import gettext_ as _
|
33
|
-
|
34
32
|
try:
|
35
33
|
import dns.resolver
|
36
34
|
import dns.exception
|
@@ -167,7 +165,7 @@ class CNAMELookupMiddleware(object):
|
|
167
165
|
elif self._domain_endswith_in_storage_domain(found_domain):
|
168
166
|
# Found it!
|
169
167
|
self.logger.info(
|
170
|
-
|
168
|
+
'Mapped %(given_domain)s to %(found_domain)s',
|
171
169
|
{'given_domain': given_domain,
|
172
170
|
'found_domain': found_domain})
|
173
171
|
if port:
|
@@ -180,8 +178,8 @@ class CNAMELookupMiddleware(object):
|
|
180
178
|
else:
|
181
179
|
# try one more deep in the chain
|
182
180
|
self.logger.debug(
|
183
|
-
|
184
|
-
|
181
|
+
'Following CNAME chain for '
|
182
|
+
'%(given_domain)s to %(found_domain)s',
|
185
183
|
{'given_domain': given_domain,
|
186
184
|
'found_domain': found_domain})
|
187
185
|
a_domain = found_domain
|
@@ -17,6 +17,7 @@ import os
|
|
17
17
|
|
18
18
|
from swift.common.constraints import valid_api_version
|
19
19
|
from swift.common.container_sync_realms import ContainerSyncRealms
|
20
|
+
from swift.common.request_helpers import append_log_info
|
20
21
|
from swift.common.swob import HTTPBadRequest, HTTPUnauthorized, wsgify
|
21
22
|
from swift.common.utils import (
|
22
23
|
config_true_value, get_logger, streq_const_time)
|
@@ -109,20 +110,17 @@ class ContainerSync(object):
|
|
109
110
|
valid = False
|
110
111
|
auth = auth.split()
|
111
112
|
if len(auth) != 3:
|
112
|
-
req.environ
|
113
|
-
'cs:not-3-args')
|
113
|
+
append_log_info(req.environ, 'cs:not-3-args')
|
114
114
|
else:
|
115
115
|
realm, nonce, sig = auth
|
116
116
|
realm_key = self.realms_conf.key(realm)
|
117
117
|
realm_key2 = self.realms_conf.key2(realm)
|
118
118
|
if not realm_key:
|
119
|
-
req.environ
|
120
|
-
'cs:no-local-realm-key')
|
119
|
+
append_log_info(req.environ, 'cs:no-local-realm-key')
|
121
120
|
else:
|
122
121
|
user_key = info.get('sync_key')
|
123
122
|
if not user_key:
|
124
|
-
req.environ
|
125
|
-
'cs:no-local-user-key')
|
123
|
+
append_log_info(req.environ, 'cs:no-local-user-key')
|
126
124
|
else:
|
127
125
|
# x-timestamp headers get shunted by gatekeeper
|
128
126
|
if 'x-backend-inbound-x-timestamp' in req.headers:
|
@@ -139,11 +137,9 @@ class ContainerSync(object):
|
|
139
137
|
realm_key2, user_key) if realm_key2 else expected
|
140
138
|
if not streq_const_time(sig, expected) and \
|
141
139
|
not streq_const_time(sig, expected2):
|
142
|
-
req.environ
|
143
|
-
'swift.log_info', []).append('cs:invalid-sig')
|
140
|
+
append_log_info(req.environ, 'cs:invalid-sig')
|
144
141
|
else:
|
145
|
-
req.environ
|
146
|
-
'swift.log_info', []).append('cs:valid')
|
142
|
+
append_log_info(req.environ, 'cs:valid')
|
147
143
|
valid = True
|
148
144
|
if not valid:
|
149
145
|
exc = HTTPUnauthorized(
|