Fix time skew when using X-Delete-After
When a client sent "X-Delete-After: <n>", the proxy and all object servers would each compute X-Delete-At as "int(time.time() + n)". However, since they don't all compute it at exactly the same time, the objects stored on disk can end up with differing values for X-Delete-At, and in that case, the object-expirer queue has multiple entries for the same object (one for each distinct X-Delete-At value). This commit makes two changes, either one of which is sufficient to fix the bug. First, after computing X-Delete-At from X-Delete-After, X-Delete-After is removed from the request's headers. Thus, the proxy computes X-Delete-At, and the object servers don't, so there's only a single value. Second, computation of X-Delete-At now uses the request's X-Timestamp instead of time.time(). In the proxy, these values are essentially the same; the proxy is responsible for setting X-Timestamp. In the object server, this ensures that all computed X-Delete-At values are identical, even if the object servers' clocks are not, or if one object server takes an extra second to respond to a PUT request. Co-Authored-By: Alistair Coles <alistairncoles@gmail.com> Change-Id: I9a1b6826c4c553f0442cfe2bb78cdf49508fa4a5 Closes-Bug: 1741371
This commit is contained in:
parent
3aa17e6dc8
commit
31c294de79
@ -16,7 +16,6 @@
|
|||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
from os.path import isdir # tighter scoped import for mocking
|
from os.path import isdir # tighter scoped import for mocking
|
||||||
import time
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
|
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
|
||||||
@ -312,6 +311,7 @@ def check_delete_headers(request):
|
|||||||
:returns: HTTPBadRequest in case of invalid values
|
:returns: HTTPBadRequest in case of invalid values
|
||||||
or None if values are ok
|
or None if values are ok
|
||||||
"""
|
"""
|
||||||
|
now = float(valid_timestamp(request))
|
||||||
if 'x-delete-after' in request.headers:
|
if 'x-delete-after' in request.headers:
|
||||||
try:
|
try:
|
||||||
x_delete_after = int(request.headers['x-delete-after'])
|
x_delete_after = int(request.headers['x-delete-after'])
|
||||||
@ -319,13 +319,14 @@ def check_delete_headers(request):
|
|||||||
raise HTTPBadRequest(request=request,
|
raise HTTPBadRequest(request=request,
|
||||||
content_type='text/plain',
|
content_type='text/plain',
|
||||||
body='Non-integer X-Delete-After')
|
body='Non-integer X-Delete-After')
|
||||||
actual_del_time = time.time() + x_delete_after
|
actual_del_time = now + x_delete_after
|
||||||
if actual_del_time < time.time():
|
if actual_del_time < now:
|
||||||
raise HTTPBadRequest(request=request,
|
raise HTTPBadRequest(request=request,
|
||||||
content_type='text/plain',
|
content_type='text/plain',
|
||||||
body='X-Delete-After in past')
|
body='X-Delete-After in past')
|
||||||
request.headers['x-delete-at'] = utils.normalize_delete_at_timestamp(
|
request.headers['x-delete-at'] = utils.normalize_delete_at_timestamp(
|
||||||
actual_del_time)
|
actual_del_time)
|
||||||
|
del request.headers['x-delete-after']
|
||||||
|
|
||||||
if 'x-delete-at' in request.headers:
|
if 'x-delete-at' in request.headers:
|
||||||
try:
|
try:
|
||||||
@ -335,7 +336,7 @@ def check_delete_headers(request):
|
|||||||
raise HTTPBadRequest(request=request, content_type='text/plain',
|
raise HTTPBadRequest(request=request, content_type='text/plain',
|
||||||
body='Non-integer X-Delete-At')
|
body='Non-integer X-Delete-At')
|
||||||
|
|
||||||
if x_delete_at < time.time() and not utils.config_true_value(
|
if x_delete_at < now and not utils.config_true_value(
|
||||||
request.headers.get('x-backend-replication', 'f')):
|
request.headers.get('x-backend-replication', 'f')):
|
||||||
raise HTTPBadRequest(request=request, content_type='text/plain',
|
raise HTTPBadRequest(request=request, content_type='text/plain',
|
||||||
body='X-Delete-At in past')
|
body='X-Delete-At in past')
|
||||||
|
@ -246,6 +246,8 @@ class BaseObjectController(Controller):
|
|||||||
if error_response:
|
if error_response:
|
||||||
return error_response
|
return error_response
|
||||||
|
|
||||||
|
req.headers['X-Timestamp'] = Timestamp.now().internal
|
||||||
|
|
||||||
req, delete_at_container, delete_at_part, \
|
req, delete_at_container, delete_at_part, \
|
||||||
delete_at_nodes = self._config_obj_expiration(req)
|
delete_at_nodes = self._config_obj_expiration(req)
|
||||||
|
|
||||||
@ -260,8 +262,6 @@ class BaseObjectController(Controller):
|
|||||||
partition, nodes = obj_ring.get_nodes(
|
partition, nodes = obj_ring.get_nodes(
|
||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
|
|
||||||
req.headers['X-Timestamp'] = Timestamp.now().internal
|
|
||||||
|
|
||||||
headers = self._backend_requests(
|
headers = self._backend_requests(
|
||||||
req, len(nodes), container_partition, container_nodes,
|
req, len(nodes), container_partition, container_nodes,
|
||||||
delete_at_container, delete_at_part, delete_at_nodes)
|
delete_at_container, delete_at_part, delete_at_nodes)
|
||||||
@ -711,14 +711,14 @@ class BaseObjectController(Controller):
|
|||||||
# update content type in case it is missing
|
# update content type in case it is missing
|
||||||
self._update_content_type(req)
|
self._update_content_type(req)
|
||||||
|
|
||||||
|
self._update_x_timestamp(req)
|
||||||
|
|
||||||
# check constraints on object name and request headers
|
# check constraints on object name and request headers
|
||||||
error_response = check_object_creation(req, self.object_name) or \
|
error_response = check_object_creation(req, self.object_name) or \
|
||||||
check_content_type(req)
|
check_content_type(req)
|
||||||
if error_response:
|
if error_response:
|
||||||
return error_response
|
return error_response
|
||||||
|
|
||||||
self._update_x_timestamp(req)
|
|
||||||
|
|
||||||
def reader():
|
def reader():
|
||||||
try:
|
try:
|
||||||
return req.environ['wsgi.input'].read(
|
return req.environ['wsgi.input'].read(
|
||||||
|
@ -19,7 +19,6 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from six.moves import range
|
from six.moves import range
|
||||||
from test import safe_repr
|
|
||||||
from test.unit import mock_check_drive
|
from test.unit import mock_check_drive
|
||||||
|
|
||||||
from swift.common.swob import Request, HTTPException
|
from swift.common.swob import Request, HTTPException
|
||||||
@ -30,14 +29,6 @@ from swift.common.constraints import MAX_OBJECT_NAME_LENGTH
|
|||||||
|
|
||||||
|
|
||||||
class TestConstraints(unittest.TestCase):
|
class TestConstraints(unittest.TestCase):
|
||||||
|
|
||||||
def assertIn(self, member, container, msg=None):
|
|
||||||
"""Copied from 2.7"""
|
|
||||||
if member not in container:
|
|
||||||
standardMsg = '%s not found in %s' % (safe_repr(member),
|
|
||||||
safe_repr(container))
|
|
||||||
self.fail(self._formatMessage(msg, standardMsg))
|
|
||||||
|
|
||||||
def test_check_metadata_empty(self):
|
def test_check_metadata_empty(self):
|
||||||
headers = {}
|
headers = {}
|
||||||
self.assertIsNone(constraints.check_metadata(Request.blank(
|
self.assertIsNone(constraints.check_metadata(Request.blank(
|
||||||
@ -145,49 +136,57 @@ class TestConstraints(unittest.TestCase):
|
|||||||
|
|
||||||
def test_check_object_creation_content_length(self):
|
def test_check_object_creation_content_length(self):
|
||||||
headers = {'Content-Length': str(constraints.MAX_FILE_SIZE),
|
headers = {'Content-Length': str(constraints.MAX_FILE_SIZE),
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name'))
|
'/', headers=headers), 'object_name'))
|
||||||
|
|
||||||
headers = {'Content-Length': str(constraints.MAX_FILE_SIZE + 1),
|
headers = {'Content-Length': str(constraints.MAX_FILE_SIZE + 1),
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_REQUEST_ENTITY_TOO_LARGE)
|
self.assertEqual(resp.status_int, HTTP_REQUEST_ENTITY_TOO_LARGE)
|
||||||
|
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name'))
|
'/', headers=headers), 'object_name'))
|
||||||
|
|
||||||
headers = {'Transfer-Encoding': 'gzip',
|
headers = {'Transfer-Encoding': 'gzip',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(Request.blank(
|
resp = constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name')
|
'/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertIn('Invalid Transfer-Encoding header value', resp.body)
|
self.assertIn('Invalid Transfer-Encoding header value', resp.body)
|
||||||
|
|
||||||
headers = {'Content-Type': 'text/plain'}
|
headers = {'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_LENGTH_REQUIRED)
|
self.assertEqual(resp.status_int, HTTP_LENGTH_REQUIRED)
|
||||||
|
|
||||||
headers = {'Content-Length': 'abc',
|
headers = {'Content-Length': 'abc',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(Request.blank(
|
resp = constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name')
|
'/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertIn('Invalid Content-Length header value', resp.body)
|
self.assertIn('Invalid Content-Length header value', resp.body)
|
||||||
|
|
||||||
headers = {'Transfer-Encoding': 'gzip,chunked',
|
headers = {'Transfer-Encoding': 'gzip,chunked',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(Request.blank(
|
resp = constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name')
|
'/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_NOT_IMPLEMENTED)
|
self.assertEqual(resp.status_int, HTTP_NOT_IMPLEMENTED)
|
||||||
|
|
||||||
def test_check_object_creation_name_length(self):
|
def test_check_object_creation_name_length(self):
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
name = 'o' * constraints.MAX_OBJECT_NAME_LENGTH
|
name = 'o' * constraints.MAX_OBJECT_NAME_LENGTH
|
||||||
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), name))
|
'/', headers=headers), name))
|
||||||
@ -202,11 +201,13 @@ class TestConstraints(unittest.TestCase):
|
|||||||
|
|
||||||
def test_check_object_creation_content_type(self):
|
def test_check_object_creation_content_type(self):
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain'}
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
self.assertIsNone(constraints.check_object_creation(Request.blank(
|
||||||
'/', headers=headers), 'object_name'))
|
'/', headers=headers), 'object_name'))
|
||||||
|
|
||||||
headers = {'Transfer-Encoding': 'chunked'}
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
@ -214,140 +215,143 @@ class TestConstraints(unittest.TestCase):
|
|||||||
|
|
||||||
def test_check_object_creation_bad_content_type(self):
|
def test_check_object_creation_bad_content_type(self):
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': '\xff\xff'}
|
'Content-Type': '\xff\xff',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertTrue('Content-Type' in resp.body)
|
self.assertIn('Content-Type', resp.body)
|
||||||
|
|
||||||
def test_check_object_creation_bad_delete_headers(self):
|
def test_check_object_creation_bad_delete_headers(self):
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-After': 'abc'}
|
'X-Delete-After': 'abc',
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertTrue('Non-integer X-Delete-After' in resp.body)
|
self.assertIn('Non-integer X-Delete-After', resp.body)
|
||||||
|
|
||||||
t = str(int(time.time() - 60))
|
t = str(int(time.time() - 60))
|
||||||
headers = {'Transfer-Encoding': 'chunked',
|
headers = {'Transfer-Encoding': 'chunked',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-At': t}
|
'X-Delete-At': t,
|
||||||
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_object_creation(
|
resp = constraints.check_object_creation(
|
||||||
Request.blank('/', headers=headers), 'object_name')
|
Request.blank('/', headers=headers), 'object_name')
|
||||||
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
self.assertEqual(resp.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertTrue('X-Delete-At in past' in resp.body)
|
self.assertIn('X-Delete-At in past', resp.body)
|
||||||
|
|
||||||
def test_check_delete_headers(self):
|
def test_check_delete_headers(self):
|
||||||
|
# x-delete-at value should be relative to the request timestamp rather
|
||||||
|
# than time.time() so separate the two to ensure the checks are robust
|
||||||
|
ts = time.time() + 100
|
||||||
|
|
||||||
# X-Delete-After
|
# X-Delete-After
|
||||||
headers = {'X-Delete-After': '60'}
|
headers = {'X-Delete-After': '600',
|
||||||
resp = constraints.check_delete_headers(
|
'X-Timestamp': str(ts)}
|
||||||
|
req = constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
self.assertTrue(isinstance(resp, Request))
|
self.assertIsInstance(req, Request)
|
||||||
self.assertTrue('x-delete-at' in resp.headers)
|
self.assertIn('x-delete-at', req.headers)
|
||||||
|
self.assertNotIn('x-delete-after', req.headers)
|
||||||
|
expected_delete_at = str(int(ts) + 600)
|
||||||
|
self.assertEqual(req.headers.get('X-Delete-At'), expected_delete_at)
|
||||||
|
|
||||||
headers = {'X-Delete-After': 'abc'}
|
headers = {'X-Delete-After': 'abc',
|
||||||
try:
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_delete_headers(
|
|
||||||
Request.blank('/', headers=headers))
|
|
||||||
except HTTPException as e:
|
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
|
||||||
self.assertTrue('Non-integer X-Delete-After' in e.body)
|
|
||||||
else:
|
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
|
||||||
|
|
||||||
headers = {'X-Delete-After': '60.1'}
|
with self.assertRaises(HTTPException) as cm:
|
||||||
try:
|
constraints.check_delete_headers(
|
||||||
resp = constraints.check_delete_headers(
|
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
except HTTPException as e:
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
self.assertIn('Non-integer X-Delete-After', cm.exception.body)
|
||||||
self.assertTrue('Non-integer X-Delete-After' in e.body)
|
|
||||||
else:
|
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
|
||||||
|
|
||||||
headers = {'X-Delete-After': '-1'}
|
headers = {'X-Delete-After': '60.1',
|
||||||
try:
|
'X-Timestamp': str(time.time())}
|
||||||
resp = constraints.check_delete_headers(
|
with self.assertRaises(HTTPException) as cm:
|
||||||
|
constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
except HTTPException as e:
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
self.assertIn('Non-integer X-Delete-After', cm.exception.body)
|
||||||
self.assertTrue('X-Delete-After in past' in e.body)
|
|
||||||
else:
|
headers = {'X-Delete-After': '-1',
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
'X-Timestamp': str(time.time())}
|
||||||
|
with self.assertRaises(HTTPException) as cm:
|
||||||
|
constraints.check_delete_headers(
|
||||||
|
Request.blank('/', headers=headers))
|
||||||
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
|
self.assertIn('X-Delete-After in past', cm.exception.body)
|
||||||
|
|
||||||
# X-Delete-At
|
# X-Delete-At
|
||||||
t = str(int(time.time() + 100))
|
delete_at = str(int(ts + 100))
|
||||||
headers = {'X-Delete-At': t}
|
headers = {'X-Delete-At': delete_at,
|
||||||
resp = constraints.check_delete_headers(
|
'X-Timestamp': str(ts)}
|
||||||
|
req = constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
self.assertTrue(isinstance(resp, Request))
|
self.assertIsInstance(req, Request)
|
||||||
self.assertTrue('x-delete-at' in resp.headers)
|
self.assertIn('x-delete-at', req.headers)
|
||||||
self.assertEqual(resp.headers.get('X-Delete-At'), t)
|
self.assertEqual(req.headers.get('X-Delete-At'), delete_at)
|
||||||
|
|
||||||
headers = {'X-Delete-At': 'abc'}
|
headers = {'X-Delete-At': 'abc',
|
||||||
try:
|
'X-Timestamp': str(ts)}
|
||||||
resp = constraints.check_delete_headers(
|
with self.assertRaises(HTTPException) as cm:
|
||||||
|
constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
except HTTPException as e:
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
self.assertIn('Non-integer X-Delete-At', cm.exception.body)
|
||||||
self.assertTrue('Non-integer X-Delete-At' in e.body)
|
|
||||||
else:
|
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
|
||||||
|
|
||||||
t = str(int(time.time() + 100)) + '.1'
|
delete_at = str(int(ts + 100)) + '.1'
|
||||||
headers = {'X-Delete-At': t}
|
headers = {'X-Delete-At': delete_at,
|
||||||
try:
|
'X-Timestamp': str(ts)}
|
||||||
resp = constraints.check_delete_headers(
|
with self.assertRaises(HTTPException) as cm:
|
||||||
|
constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
except HTTPException as e:
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
self.assertIn('Non-integer X-Delete-At', cm.exception.body)
|
||||||
self.assertTrue('Non-integer X-Delete-At' in e.body)
|
|
||||||
else:
|
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
|
||||||
|
|
||||||
t = str(int(time.time()))
|
delete_at = str(int(ts - 1))
|
||||||
headers = {'X-Delete-At': t}
|
headers = {'X-Delete-At': delete_at,
|
||||||
try:
|
'X-Timestamp': str(ts)}
|
||||||
resp = constraints.check_delete_headers(
|
with self.assertRaises(HTTPException) as cm:
|
||||||
|
constraints.check_delete_headers(
|
||||||
Request.blank('/', headers=headers))
|
Request.blank('/', headers=headers))
|
||||||
except HTTPException as e:
|
self.assertEqual(cm.exception.status_int, HTTP_BAD_REQUEST)
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
self.assertIn('X-Delete-At in past', cm.exception.body)
|
||||||
self.assertTrue('X-Delete-At in past' in e.body)
|
|
||||||
else:
|
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
|
||||||
|
|
||||||
t = str(int(time.time() - 1))
|
def test_check_delete_headers_removes_delete_after(self):
|
||||||
headers = {'X-Delete-At': t}
|
t = time.time()
|
||||||
try:
|
headers = {'Content-Length': '0',
|
||||||
resp = constraints.check_delete_headers(
|
'Content-Type': 'text/plain',
|
||||||
Request.blank('/', headers=headers))
|
'X-Delete-After': '42',
|
||||||
except HTTPException as e:
|
'X-Delete-At': str(int(t) + 40),
|
||||||
self.assertEqual(e.status_int, HTTP_BAD_REQUEST)
|
'X-Timestamp': str(t)}
|
||||||
self.assertTrue('X-Delete-At in past' in e.body)
|
req = Request.blank('/', headers=headers)
|
||||||
else:
|
constraints.check_delete_headers(req)
|
||||||
self.fail("Should have failed with HTTPBadRequest")
|
self.assertNotIn('X-Delete-After', req.headers)
|
||||||
|
self.assertEqual(req.headers['X-Delete-At'], str(int(t) + 42))
|
||||||
|
|
||||||
def test_check_delete_headers_sets_delete_at(self):
|
def test_check_delete_headers_sets_delete_at(self):
|
||||||
t = time.time() + 1000
|
t = time.time()
|
||||||
|
expected = str(int(t) + 1000)
|
||||||
# check delete-at is passed through
|
# check delete-at is passed through
|
||||||
headers = {'Content-Length': '0',
|
headers = {'Content-Length': '0',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-At': str(int(t))}
|
'X-Delete-At': expected,
|
||||||
|
'X-Timestamp': str(t)}
|
||||||
req = Request.blank('/', headers=headers)
|
req = Request.blank('/', headers=headers)
|
||||||
constraints.check_delete_headers(req)
|
constraints.check_delete_headers(req)
|
||||||
self.assertTrue('X-Delete-At' in req.headers)
|
self.assertIn('X-Delete-At', req.headers)
|
||||||
self.assertEqual(req.headers['X-Delete-At'], str(int(t)))
|
self.assertEqual(req.headers['X-Delete-At'], expected)
|
||||||
|
|
||||||
# check delete-after is converted to delete-at
|
# check delete-after is converted to delete-at
|
||||||
headers = {'Content-Length': '0',
|
headers = {'Content-Length': '0',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-After': '42'}
|
'X-Delete-After': '42',
|
||||||
|
'X-Timestamp': str(t)}
|
||||||
req = Request.blank('/', headers=headers)
|
req = Request.blank('/', headers=headers)
|
||||||
with mock.patch('time.time', lambda: t):
|
constraints.check_delete_headers(req)
|
||||||
constraints.check_delete_headers(req)
|
self.assertIn('X-Delete-At', req.headers)
|
||||||
self.assertTrue('X-Delete-At' in req.headers)
|
|
||||||
expected = str(int(t) + 42)
|
expected = str(int(t) + 42)
|
||||||
self.assertEqual(req.headers['X-Delete-At'], expected)
|
self.assertEqual(req.headers['X-Delete-At'], expected)
|
||||||
|
|
||||||
@ -355,21 +359,21 @@ class TestConstraints(unittest.TestCase):
|
|||||||
headers = {'Content-Length': '0',
|
headers = {'Content-Length': '0',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-After': '42',
|
'X-Delete-After': '42',
|
||||||
'X-Delete-At': str(int(t) + 40)}
|
'X-Delete-At': str(int(t) + 40),
|
||||||
|
'X-Timestamp': str(t)}
|
||||||
req = Request.blank('/', headers=headers)
|
req = Request.blank('/', headers=headers)
|
||||||
with mock.patch('time.time', lambda: t):
|
constraints.check_delete_headers(req)
|
||||||
constraints.check_delete_headers(req)
|
self.assertIn('X-Delete-At', req.headers)
|
||||||
self.assertTrue('X-Delete-At' in req.headers)
|
|
||||||
self.assertEqual(req.headers['X-Delete-At'], expected)
|
self.assertEqual(req.headers['X-Delete-At'], expected)
|
||||||
|
|
||||||
headers = {'Content-Length': '0',
|
headers = {'Content-Length': '0',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'X-Delete-After': '42',
|
'X-Delete-After': '42',
|
||||||
'X-Delete-At': str(int(t) + 44)}
|
'X-Delete-At': str(int(t) + 44),
|
||||||
|
'X-Timestamp': str(t)}
|
||||||
req = Request.blank('/', headers=headers)
|
req = Request.blank('/', headers=headers)
|
||||||
with mock.patch('time.time', lambda: t):
|
constraints.check_delete_headers(req)
|
||||||
constraints.check_delete_headers(req)
|
self.assertIn('X-Delete-At', req.headers)
|
||||||
self.assertTrue('X-Delete-At' in req.headers)
|
|
||||||
self.assertEqual(req.headers['X-Delete-At'], expected)
|
self.assertEqual(req.headers['X-Delete-At'], expected)
|
||||||
|
|
||||||
def test_check_drive_invalid_path(self):
|
def test_check_drive_invalid_path(self):
|
||||||
@ -473,10 +477,10 @@ class TestConstraints(unittest.TestCase):
|
|||||||
|
|
||||||
def test_validate_constraints(self):
|
def test_validate_constraints(self):
|
||||||
c = constraints
|
c = constraints
|
||||||
self.assertTrue(c.MAX_META_OVERALL_SIZE > c.MAX_META_NAME_LENGTH)
|
self.assertGreater(c.MAX_META_OVERALL_SIZE, c.MAX_META_NAME_LENGTH)
|
||||||
self.assertTrue(c.MAX_META_OVERALL_SIZE > c.MAX_META_VALUE_LENGTH)
|
self.assertGreater(c.MAX_META_OVERALL_SIZE, c.MAX_META_VALUE_LENGTH)
|
||||||
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_NAME_LENGTH)
|
self.assertGreater(c.MAX_HEADER_SIZE, c.MAX_META_NAME_LENGTH)
|
||||||
self.assertTrue(c.MAX_HEADER_SIZE > c.MAX_META_VALUE_LENGTH)
|
self.assertGreater(c.MAX_HEADER_SIZE, c.MAX_META_VALUE_LENGTH)
|
||||||
|
|
||||||
def test_check_account_format(self):
|
def test_check_account_format(self):
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
@ -501,14 +505,11 @@ class TestConstraints(unittest.TestCase):
|
|||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/v/a/c/o', headers={
|
'/v/a/c/o', headers={
|
||||||
'X-Versions-Location': versions_location})
|
'X-Versions-Location': versions_location})
|
||||||
try:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
constraints.check_container_format(
|
constraints.check_container_format(
|
||||||
req, req.headers['X-Versions-Location'])
|
req, req.headers['X-Versions-Location'])
|
||||||
except HTTPException as e:
|
self.assertTrue(cm.exception.body.startswith(
|
||||||
self.assertTrue(e.body.startswith('Container name cannot'))
|
'Container name cannot'))
|
||||||
else:
|
|
||||||
self.fail('check_container_format did not raise error for %r' %
|
|
||||||
req.headers['X-Versions-Location'])
|
|
||||||
|
|
||||||
def test_valid_api_version(self):
|
def test_valid_api_version(self):
|
||||||
version = 'v1'
|
version = 'v1'
|
||||||
|
@ -5773,16 +5773,16 @@ class TestObjectController(unittest.TestCase):
|
|||||||
given_args[5], 'sda1', policy])
|
given_args[5], 'sda1', policy])
|
||||||
|
|
||||||
def test_GET_but_expired(self):
|
def test_GET_but_expired(self):
|
||||||
|
# Start off with an existing object that will expire
|
||||||
now = time()
|
now = time()
|
||||||
test_time = now + 10000
|
delete_at_timestamp = int(now + 100)
|
||||||
delete_at_timestamp = int(test_time + 100)
|
|
||||||
delete_at_container = str(
|
delete_at_container = str(
|
||||||
delete_at_timestamp /
|
delete_at_timestamp /
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
self.object_controller.expiring_objects_container_divisor *
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
self.object_controller.expiring_objects_container_divisor)
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time - 2000),
|
headers={'X-Timestamp': normalize_timestamp(now),
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
'X-Delete-At': str(delete_at_timestamp),
|
||||||
'X-Delete-At-Container': delete_at_container,
|
'X-Delete-At-Container': delete_at_container,
|
||||||
'Content-Length': '4',
|
'Content-Length': '4',
|
||||||
@ -5791,50 +5791,29 @@ class TestObjectController(unittest.TestCase):
|
|||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 201)
|
self.assertEqual(resp.status_int, 201)
|
||||||
|
|
||||||
|
# It expires in the future, so it's accessible via GET
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'},
|
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'},
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time)})
|
headers={'X-Timestamp': normalize_timestamp(now)})
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 200)
|
|
||||||
|
|
||||||
delete_at_timestamp = int(now + 1)
|
|
||||||
delete_at_container = str(
|
|
||||||
delete_at_timestamp /
|
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
|
||||||
put_timestamp = normalize_timestamp(test_time - 1000)
|
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'PUT'},
|
|
||||||
headers={'X-Timestamp': put_timestamp,
|
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
|
||||||
'X-Delete-At-Container': delete_at_container,
|
|
||||||
'Content-Length': '4',
|
|
||||||
'Content-Type': 'application/octet-stream'})
|
|
||||||
req.body = 'TEST'
|
|
||||||
|
|
||||||
# fix server time to now: delete-at is in future, verify GET is ok
|
|
||||||
with mock.patch('swift.obj.server.time.time', return_value=now):
|
with mock.patch('swift.obj.server.time.time', return_value=now):
|
||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 201)
|
self.assertEqual(resp.status_int, 200)
|
||||||
|
|
||||||
req = Request.blank(
|
# It expires in the past, so it's not accessible via GET...
|
||||||
'/sda1/p/a/c/o',
|
req = Request.blank(
|
||||||
environ={'REQUEST_METHOD': 'GET'},
|
'/sda1/p/a/c/o',
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time)})
|
environ={'REQUEST_METHOD': 'GET'},
|
||||||
|
headers={'X-Timestamp': normalize_timestamp(
|
||||||
|
delete_at_timestamp + 1)})
|
||||||
|
with mock.patch('swift.obj.server.time.time',
|
||||||
|
return_value=delete_at_timestamp + 1):
|
||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 200)
|
self.assertEqual(resp.status_int, 404)
|
||||||
|
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
||||||
|
utils.Timestamp(now))
|
||||||
|
|
||||||
# fix server time to now + 2: delete-at is in past, verify GET fails...
|
with mock.patch('swift.obj.server.time.time',
|
||||||
with mock.patch('swift.obj.server.time.time', return_value=now + 2):
|
return_value=delete_at_timestamp + 1):
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'GET'},
|
|
||||||
headers={'X-Timestamp': normalize_timestamp(now + 2)})
|
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 404)
|
|
||||||
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
|
||||||
utils.Timestamp(put_timestamp))
|
|
||||||
# ...unless X-Backend-Replication is sent
|
# ...unless X-Backend-Replication is sent
|
||||||
expected = {
|
expected = {
|
||||||
'GET': 'TEST',
|
'GET': 'TEST',
|
||||||
@ -5843,22 +5822,24 @@ class TestObjectController(unittest.TestCase):
|
|||||||
for meth, expected_body in expected.items():
|
for meth, expected_body in expected.items():
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o', method=meth,
|
'/sda1/p/a/c/o', method=meth,
|
||||||
headers={'X-Timestamp': normalize_timestamp(now + 2),
|
headers={'X-Timestamp':
|
||||||
|
normalize_timestamp(delete_at_timestamp + 1),
|
||||||
'X-Backend-Replication': 'True'})
|
'X-Backend-Replication': 'True'})
|
||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 200)
|
self.assertEqual(resp.status_int, 200)
|
||||||
self.assertEqual(expected_body, resp.body)
|
self.assertEqual(expected_body, resp.body)
|
||||||
|
|
||||||
def test_HEAD_but_expired(self):
|
def test_HEAD_but_expired(self):
|
||||||
test_time = time() + 10000
|
# We have an object that expires in the future
|
||||||
delete_at_timestamp = int(test_time + 100)
|
now = time()
|
||||||
|
delete_at_timestamp = int(now + 100)
|
||||||
delete_at_container = str(
|
delete_at_container = str(
|
||||||
delete_at_timestamp /
|
delete_at_timestamp /
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
self.object_controller.expiring_objects_container_divisor *
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
self.object_controller.expiring_objects_container_divisor)
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time - 2000),
|
headers={'X-Timestamp': normalize_timestamp(now),
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
'X-Delete-At': str(delete_at_timestamp),
|
||||||
'X-Delete-At-Container': delete_at_container,
|
'X-Delete-At-Container': delete_at_container,
|
||||||
'Content-Length': '4',
|
'Content-Length': '4',
|
||||||
@ -5867,26 +5848,43 @@ class TestObjectController(unittest.TestCase):
|
|||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 201)
|
self.assertEqual(resp.status_int, 201)
|
||||||
|
|
||||||
|
# It's accessible since it expires in the future
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o',
|
'/sda1/p/a/c/o',
|
||||||
environ={'REQUEST_METHOD': 'HEAD'},
|
environ={'REQUEST_METHOD': 'HEAD'},
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time)})
|
headers={'X-Timestamp': normalize_timestamp(now)})
|
||||||
resp = req.get_response(self.object_controller)
|
with mock.patch('swift.obj.server.time.time', return_value=now):
|
||||||
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 200)
|
self.assertEqual(resp.status_int, 200)
|
||||||
|
|
||||||
# fix server time to now: delete-at is in future, verify GET is ok
|
# It's not accessible now since it expires in the past
|
||||||
|
req = Request.blank(
|
||||||
|
'/sda1/p/a/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'HEAD'},
|
||||||
|
headers={'X-Timestamp': normalize_timestamp(
|
||||||
|
delete_at_timestamp + 1)})
|
||||||
|
with mock.patch('swift.obj.server.time.time',
|
||||||
|
return_value=delete_at_timestamp + 1):
|
||||||
|
resp = req.get_response(self.object_controller)
|
||||||
|
self.assertEqual(resp.status_int, 404)
|
||||||
|
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
||||||
|
utils.Timestamp(now))
|
||||||
|
|
||||||
|
def test_POST_but_expired(self):
|
||||||
now = time()
|
now = time()
|
||||||
with mock.patch('swift.obj.server.time.time', return_value=now):
|
delete_at_timestamp = int(now + 100)
|
||||||
delete_at_timestamp = int(now + 1)
|
delete_at_container = str(
|
||||||
delete_at_container = str(
|
delete_at_timestamp /
|
||||||
delete_at_timestamp /
|
self.object_controller.expiring_objects_container_divisor *
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
self.object_controller.expiring_objects_container_divisor)
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
|
||||||
put_timestamp = normalize_timestamp(test_time - 1000)
|
# We recreate the test object every time to ensure a clean test; a
|
||||||
|
# POST may change attributes of the object, so it's not safe to
|
||||||
|
# re-use.
|
||||||
|
def recreate_test_object(when):
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o',
|
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
environ={'REQUEST_METHOD': 'PUT'},
|
headers={'X-Timestamp': normalize_timestamp(when),
|
||||||
headers={'X-Timestamp': put_timestamp,
|
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
'X-Delete-At': str(delete_at_timestamp),
|
||||||
'X-Delete-At-Container': delete_at_container,
|
'X-Delete-At-Container': delete_at_container,
|
||||||
'Content-Length': '4',
|
'Content-Length': '4',
|
||||||
@ -5894,76 +5892,29 @@ class TestObjectController(unittest.TestCase):
|
|||||||
req.body = 'TEST'
|
req.body = 'TEST'
|
||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 201)
|
self.assertEqual(resp.status_int, 201)
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'HEAD'},
|
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time)})
|
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 200)
|
|
||||||
|
|
||||||
with mock.patch('swift.obj.server.time.time', return_value=now + 2):
|
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'HEAD'},
|
|
||||||
headers={'X-Timestamp': normalize_timestamp(now + 2)})
|
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 404)
|
|
||||||
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
|
||||||
utils.Timestamp(put_timestamp))
|
|
||||||
|
|
||||||
def test_POST_but_expired(self):
|
|
||||||
test_time = time() + 10000
|
|
||||||
delete_at_timestamp = int(test_time + 100)
|
|
||||||
delete_at_container = str(
|
|
||||||
delete_at_timestamp /
|
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time - 2000),
|
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
|
||||||
'X-Delete-At-Container': delete_at_container,
|
|
||||||
'Content-Length': '4',
|
|
||||||
'Content-Type': 'application/octet-stream'})
|
|
||||||
req.body = 'TEST'
|
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
|
# You can POST to a not-yet-expired object
|
||||||
|
recreate_test_object(now)
|
||||||
|
the_time = now + 1
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o',
|
'/sda1/p/a/c/o',
|
||||||
environ={'REQUEST_METHOD': 'POST'},
|
environ={'REQUEST_METHOD': 'POST'},
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time - 1500)})
|
headers={'X-Timestamp': normalize_timestamp(the_time)})
|
||||||
resp = req.get_response(self.object_controller)
|
with mock.patch('swift.obj.server.time.time', return_value=the_time):
|
||||||
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 202)
|
self.assertEqual(resp.status_int, 202)
|
||||||
|
|
||||||
delete_at_timestamp = int(time() + 2)
|
# You cannot POST to an expired object
|
||||||
delete_at_container = str(
|
now += 2
|
||||||
delete_at_timestamp /
|
recreate_test_object(now)
|
||||||
self.object_controller.expiring_objects_container_divisor *
|
the_time = delete_at_timestamp + 1
|
||||||
self.object_controller.expiring_objects_container_divisor)
|
|
||||||
req = Request.blank(
|
req = Request.blank(
|
||||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
'/sda1/p/a/c/o',
|
||||||
headers={'X-Timestamp': normalize_timestamp(test_time - 1000),
|
environ={'REQUEST_METHOD': 'POST'},
|
||||||
'X-Delete-At': str(delete_at_timestamp),
|
headers={'X-Timestamp': normalize_timestamp(the_time)})
|
||||||
'X-Delete-At-Container': delete_at_container,
|
with mock.patch('swift.obj.server.time.time', return_value=the_time):
|
||||||
'Content-Length': '4',
|
|
||||||
'Content-Type': 'application/octet-stream'})
|
|
||||||
req.body = 'TEST'
|
|
||||||
resp = req.get_response(self.object_controller)
|
|
||||||
self.assertEqual(resp.status_int, 201)
|
|
||||||
|
|
||||||
orig_time = object_server.time.time
|
|
||||||
try:
|
|
||||||
t = time() + 3
|
|
||||||
object_server.time.time = lambda: t
|
|
||||||
req = Request.blank(
|
|
||||||
'/sda1/p/a/c/o',
|
|
||||||
environ={'REQUEST_METHOD': 'POST'},
|
|
||||||
headers={'X-Timestamp': normalize_timestamp(time())})
|
|
||||||
resp = req.get_response(self.object_controller)
|
resp = req.get_response(self.object_controller)
|
||||||
self.assertEqual(resp.status_int, 404)
|
self.assertEqual(resp.status_int, 404)
|
||||||
finally:
|
|
||||||
object_server.time.time = orig_time
|
|
||||||
|
|
||||||
def test_DELETE_but_expired(self):
|
def test_DELETE_but_expired(self):
|
||||||
test_time = time() + 10000
|
test_time = time() + 10000
|
||||||
|
Loading…
Reference in New Issue
Block a user