Skip to content

Commit cbba65a

Browse files
tipabuindianwhocodes
authored andcommitted
quotas: Add account-level per-policy quotas
Reseller admins can set new headers on accounts like X-Account-Quota-Bytes-Policy-<policy-name>: <quota> This may be done to limit consumption of a faster, all-flash policy, for example. This is independent of the existing X-Account-Meta-Quota-Bytes header, which continues to limit the total storage for an account across all policies. Change-Id: Ib25c2f667e5b81301f8c67375644981a13487cfe
1 parent 9a1bfb8 commit cbba65a

File tree

4 files changed

+192
-42
lines changed

4 files changed

+192
-42
lines changed

doc/source/api/container_quotas.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _container_quotas:
2+
13
================
24
Container quotas
35
================

etc/proxy-server.conf-sample

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,11 +1110,11 @@ use = egg:swift#dlo
11101110
# Time limit on GET requests (seconds)
11111111
# max_get_time = 86400
11121112

1113-
# Note: Put after auth in the pipeline.
1113+
# Note: Put after auth and server-side copy in the pipeline.
11141114
[filter:container-quotas]
11151115
use = egg:swift#container_quotas
11161116

1117-
# Note: Put after auth in the pipeline.
1117+
# Note: Put after auth and server-side copy in the pipeline.
11181118
[filter:account-quotas]
11191119
use = egg:swift#account_quotas
11201120

swift/common/middleware/account_quotas.py

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@
1919
allowed.
2020
2121
``account_quotas`` uses the ``x-account-meta-quota-bytes`` metadata entry to
22-
store the quota. Write requests to this metadata entry are only permitted for
23-
resellers. There is no quota limit if ``x-account-meta-quota-bytes`` is not
24-
set.
22+
store the overall account quota. Write requests to this metadata entry are
23+
only permitted for resellers. There is no overall account quota limit if
24+
``x-account-meta-quota-bytes`` is not set.
25+
26+
Additionally, account quotas may be set for each storage policy, using metadata
27+
of the form ``x-account-quota-bytes-policy-<policy name>``. Again, only
28+
resellers may update these metadata, and there will be no limit for a
29+
particular policy if the corresponding metadata is not set.
30+
31+
.. note::
32+
Per-policy quotas need not sum to the overall account quota, and the sum of
33+
all :ref:`container_quotas` for a given policy need not sum to the account's
34+
policy quota.
2535
2636
The ``account_quotas`` middleware should be added to the pipeline in your
2737
``/etc/swift/proxy-server.conf`` file just after any auth middleware.
@@ -55,7 +65,8 @@
5565
from swift.common.swob import HTTPForbidden, HTTPBadRequest, \
5666
HTTPRequestEntityTooLarge, wsgify
5767
from swift.common.registry import register_swift_info
58-
from swift.proxy.controllers.base import get_account_info
68+
from swift.common.storage_policy import POLICIES
69+
from swift.proxy.controllers.base import get_account_info, get_container_info
5970

6071

6172
class AccountQuotaMiddleware(object):
@@ -68,29 +79,49 @@ def __init__(self, app, *args, **kwargs):
6879
self.app = app
6980

7081
def handle_account(self, request):
71-
# account request, so we pay attention to the quotas
72-
new_quota = request.headers.get(
73-
'X-Account-Meta-Quota-Bytes')
74-
if request.headers.get(
75-
'X-Remove-Account-Meta-Quota-Bytes'):
76-
new_quota = 0 # X-Remove dominates if both are present
77-
78-
if request.environ.get('reseller_request') is True:
79-
if new_quota and not new_quota.isdigit():
80-
return HTTPBadRequest()
81-
return self.app
82-
83-
# deny quota set for non-reseller
84-
if new_quota is not None:
85-
return HTTPForbidden()
86-
return self.app
82+
if request.method in ("POST", "PUT"):
83+
# account request, so we pay attention to the quotas
84+
new_quotas = {}
85+
new_quotas[None] = request.headers.get(
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+
112+
resp = request.get_response(self.app)
113+
# Non-resellers can't update quotas, but they *can* see them
114+
for policy in POLICIES:
115+
infix = 'Quota-Bytes-Policy'
116+
value = resp.headers.get('X-Account-Sysmeta-%s-%d' % (
117+
infix, policy.idx))
118+
if value:
119+
resp.headers['X-Account-%s-%s' % (infix, policy.name)] = value
120+
return resp
87121

88122
@wsgify
89123
def __call__(self, request):
90124

91-
if request.method not in ("POST", "PUT"):
92-
return self.app
93-
94125
try:
95126
ver, account, container, obj = request.split_path(
96127
2, 4, rest_with_last=True)
@@ -102,14 +133,15 @@ def __call__(self, request):
102133
# container or object request; even if the quota headers are set
103134
# in the request, they're meaningless
104135

105-
if request.method == "POST" or not obj:
136+
if not (request.method == "PUT" and obj):
106137
return self.app
107138
# OK, object PUT
108139

109140
if request.environ.get('reseller_request') is True:
110141
# but resellers aren't constrained by quotas :-)
111142
return self.app
112143

144+
# Object PUT request
113145
content_length = (request.content_length or 0)
114146

115147
account_info = get_account_info(request.environ, self.app,
@@ -119,24 +151,50 @@ def __call__(self, request):
119151
try:
120152
quota = int(account_info['meta'].get('quota-bytes', -1))
121153
except ValueError:
122-
return self.app
123-
if quota < 0:
124-
return self.app
125-
126-
new_size = int(account_info['bytes']) + content_length
127-
if quota < new_size:
128-
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
129-
if 'swift.authorize' in request.environ:
130-
orig_authorize = request.environ['swift.authorize']
154+
quota = -1
155+
if quota >= 0:
156+
new_size = int(account_info['bytes']) + content_length
157+
if quota < new_size:
158+
resp = HTTPRequestEntityTooLarge(body='Upload exceeds quota.')
159+
if 'swift.authorize' in request.environ:
160+
orig_authorize = request.environ['swift.authorize']
161+
162+
def reject_authorize(*args, **kwargs):
163+
aresp = orig_authorize(*args, **kwargs)
164+
if aresp:
165+
return aresp
166+
return resp
167+
request.environ['swift.authorize'] = reject_authorize
168+
else:
169+
return resp
131170

132-
def reject_authorize(*args, **kwargs):
133-
aresp = orig_authorize(*args, **kwargs)
134-
if aresp:
135-
return aresp
171+
container_info = get_container_info(request.environ, self.app,
172+
swift_source='AQ')
173+
if not container_info:
174+
return self.app
175+
policy_idx = container_info['storage_policy']
176+
sysmeta_key = 'quota-bytes-policy-%s' % policy_idx
177+
try:
178+
policy_quota = int(account_info['sysmeta'].get(sysmeta_key, -1))
179+
except ValueError:
180+
policy_quota = -1
181+
if policy_quota >= 0:
182+
policy_stats = account_info['storage_policies'].get(policy_idx, {})
183+
new_size = int(policy_stats.get('bytes', 0)) + content_length
184+
if policy_quota < new_size:
185+
resp = HTTPRequestEntityTooLarge(
186+
body='Upload exceeds policy quota.')
187+
if 'swift.authorize' in request.environ:
188+
orig_authorize = request.environ['swift.authorize']
189+
190+
def reject_authorize(*args, **kwargs):
191+
aresp = orig_authorize(*args, **kwargs)
192+
if aresp:
193+
return aresp
194+
return resp
195+
request.environ['swift.authorize'] = reject_authorize
196+
else:
136197
return resp
137-
request.environ['swift.authorize'] = reject_authorize
138-
else:
139-
return resp
140198

141199
return self.app
142200

test/unit/common/middleware/test_account_quotas.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from swift.common.middleware import account_quotas, copy
2020

21+
from test.unit import patch_policies
2122
from test.unit.common.middleware.helpers import FakeSwift
2223

2324

@@ -53,6 +54,8 @@ def setUp(self):
5354
self.app = FakeSwift()
5455
self.app.register('HEAD', '/v1/a', HTTPOk, {
5556
'x-account-bytes-used': '1000'})
57+
self.app.register('HEAD', '/v1/a/c', HTTPOk, {
58+
'x-backend-storage-policy-index': '1'})
5659
self.app.register('POST', '/v1/a', HTTPOk, {})
5760
self.app.register('PUT', '/v1/a/c/o', HTTPOk, {})
5861

@@ -128,6 +131,48 @@ def test_exceed_bytes_quota(self):
128131
self.assertEqual(res.status_int, 413)
129132
self.assertEqual(res.body, b'Upload exceeds quota.')
130133

134+
@patch_policies
135+
def test_exceed_per_policy_quota(self):
136+
self.app.register('HEAD', '/v1/a', HTTPOk, {
137+
'x-account-bytes-used': '100',
138+
'x-account-storage-policy-unu-bytes-used': '100',
139+
'x-account-sysmeta-quota-bytes-policy-1': '10',
140+
'x-account-meta-quota-bytes': '1000'})
141+
app = account_quotas.AccountQuotaMiddleware(self.app)
142+
cache = FakeCache(None)
143+
req = Request.blank('/v1/a/c/o',
144+
environ={'REQUEST_METHOD': 'PUT',
145+
'swift.cache': cache})
146+
res = req.get_response(app)
147+
self.assertEqual(res.status_int, 413)
148+
self.assertEqual(res.body, b'Upload exceeds policy quota.')
149+
150+
@patch_policies
151+
def test_policy_quota_translation(self):
152+
def do_test(method):
153+
self.app.register(method, '/v1/a', HTTPOk, {
154+
'x-account-bytes-used': '100',
155+
'x-account-storage-policy-unu-bytes-used': '100',
156+
'x-account-sysmeta-quota-bytes-policy-1': '10',
157+
'x-account-meta-quota-bytes': '1000'})
158+
app = account_quotas.AccountQuotaMiddleware(self.app)
159+
cache = FakeCache(None)
160+
req = Request.blank('/v1/a', method=method, environ={
161+
'swift.cache': cache})
162+
res = req.get_response(app)
163+
self.assertEqual(res.status_int, 200)
164+
self.assertEqual(res.headers.get(
165+
'X-Account-Meta-Quota-Bytes'), '1000')
166+
self.assertEqual(res.headers.get(
167+
'X-Account-Sysmeta-Quota-Bytes-Policy-1'), '10')
168+
self.assertEqual(res.headers.get(
169+
'X-Account-Quota-Bytes-Policy-Unu'), '10')
170+
self.assertEqual(res.headers.get(
171+
'X-Account-Storage-Policy-Unu-Bytes-Used'), '100')
172+
173+
do_test('GET')
174+
do_test('HEAD')
175+
131176
def test_exceed_quota_not_authorized(self):
132177
self.app.register('HEAD', '/v1/a', HTTPOk, {
133178
'x-account-bytes-used': '1000',
@@ -335,6 +380,19 @@ def test_invalid_quotas(self):
335380
'reseller_request': True})
336381
res = req.get_response(app)
337382
self.assertEqual(res.status_int, 400)
383+
self.assertEqual(self.app.calls, [])
384+
385+
def test_invalid_policy_quota(self):
386+
app = account_quotas.AccountQuotaMiddleware(self.app)
387+
cache = FakeCache(None)
388+
req = Request.blank('/v1/a', environ={
389+
'REQUEST_METHOD': 'POST',
390+
'swift.cache': cache,
391+
'HTTP_X_ACCOUNT_QUOTA_BYTES_POLICY_POLICY_0': 'abc',
392+
'reseller_request': True})
393+
res = req.get_response(app)
394+
self.assertEqual(res.status_int, 400)
395+
self.assertEqual(self.app.calls, [])
338396

339397
def test_valid_quotas_admin(self):
340398
app = account_quotas.AccountQuotaMiddleware(self.app)
@@ -345,6 +403,18 @@ def test_valid_quotas_admin(self):
345403
'HTTP_X_ACCOUNT_META_QUOTA_BYTES': '100'})
346404
res = req.get_response(app)
347405
self.assertEqual(res.status_int, 403)
406+
self.assertEqual(self.app.calls, [])
407+
408+
def test_valid_policy_quota_admin(self):
409+
app = account_quotas.AccountQuotaMiddleware(self.app)
410+
cache = FakeCache(None)
411+
req = Request.blank('/v1/a', environ={
412+
'REQUEST_METHOD': 'POST',
413+
'swift.cache': cache,
414+
'HTTP_X_ACCOUNT_QUOTA_BYTES_POLICY_POLICY_0': '100'})
415+
res = req.get_response(app)
416+
self.assertEqual(res.status_int, 403)
417+
self.assertEqual(self.app.calls, [])
348418

349419
def test_valid_quotas_reseller(self):
350420
app = account_quotas.AccountQuotaMiddleware(self.app)
@@ -356,6 +426,24 @@ def test_valid_quotas_reseller(self):
356426
'reseller_request': True})
357427
res = req.get_response(app)
358428
self.assertEqual(res.status_int, 200)
429+
self.assertEqual(self.app.calls_with_headers, [
430+
('POST', '/v1/a', {'Host': 'localhost:80',
431+
'X-Account-Meta-Quota-Bytes': '100'})])
432+
433+
def test_valid_policy_quota_reseller(self):
434+
app = account_quotas.AccountQuotaMiddleware(self.app)
435+
cache = FakeCache(None)
436+
req = Request.blank('/v1/a', environ={
437+
'REQUEST_METHOD': 'POST',
438+
'swift.cache': cache,
439+
'HTTP_X_ACCOUNT_QUOTA_BYTES_POLICY_POLICY_0': '100',
440+
'reseller_request': True})
441+
res = req.get_response(app)
442+
self.assertEqual(res.status_int, 200)
443+
self.assertEqual(self.app.calls_with_headers, [
444+
('POST', '/v1/a', {
445+
'Host': 'localhost:80',
446+
'X-Account-Sysmeta-Quota-Bytes-Policy-0': '100'})])
359447

360448
def test_delete_quotas(self):
361449
app = account_quotas.AccountQuotaMiddleware(self.app)
@@ -414,6 +502,8 @@ def setUp(self):
414502
self.headers = []
415503
self.app = FakeSwift()
416504
self.app.register('HEAD', '/v1/a', HTTPOk, self.headers)
505+
self.app.register('HEAD', '/v1/a/c', HTTPOk, {
506+
'x-backend-storage-policy-index': '1'})
417507
self.app.register('GET', '/v1/a/c2/o2', HTTPOk, {
418508
'content-length': '1000'})
419509
self.aq_filter = account_quotas.filter_factory({})(self.app)

0 commit comments

Comments
 (0)