Use X-Timestamp when checking object expiration
In the object server's PUT, POST, and DELETE handlers, we use the request's X-Timestamp value for checking object expiration. In the GET and HEAD handlers, we use it if present, but default to the current time. That way, one can still use curl to make direct object GET or HEAD requests as before. If one object server's clock is ahead of the proxy server's clock for some reason, and a client makes a POST request to update X-Delete-At, then the skewed-time object server may refuse the new X-Delete-At value. In a cluster where two of the three replicas for an object live on the same time-skewed node, this can result in confusing behavior for clients. A client can make a POST request to update X-Delete-At, receive a 400, and then discover later that the X-Delete-At value was updated anyway, since one object server accepted the POST and replication spread the new metadata around. DELETE is somewhat less confusing. The client might get a spurious 404 in the above case, but the object will still be removed. For PUT, an object server with a slow clock might refuse to overwrite an object with an "older" one because it believes the on-disk object is newer than the current time. Change-Id: I10c28f97d4c6aca1d64bef3b93506cfbb50ade30
This commit is contained in:
parent
765f6530e8
commit
7a7677868d
@ -2203,7 +2203,7 @@ class BaseDiskFile(object):
|
||||
return cls(mgr, device_path, None, partition, _datadir=hash_dir_path,
|
||||
policy=policy)
|
||||
|
||||
def open(self, modernize=False):
|
||||
def open(self, modernize=False, current_time=None):
|
||||
"""
|
||||
Open the object.
|
||||
|
||||
@ -2215,6 +2215,9 @@ class BaseDiskFile(object):
|
||||
Currently, this means adding metadata checksums if none are
|
||||
present.
|
||||
|
||||
:param current_time: Unix time used in checking expiration. If not
|
||||
present, the current time will be used.
|
||||
|
||||
.. note::
|
||||
|
||||
An implementation is allowed to raise any of the following
|
||||
@ -2254,7 +2257,7 @@ class BaseDiskFile(object):
|
||||
if not self._data_file:
|
||||
raise self._construct_exception_from_ts_file(**file_info)
|
||||
self._fp = self._construct_from_data_file(
|
||||
modernize=modernize, **file_info)
|
||||
current_time=current_time, modernize=modernize, **file_info)
|
||||
# This method must populate the internal _metadata attribute.
|
||||
self._metadata = self._metadata or {}
|
||||
return self
|
||||
@ -2353,7 +2356,7 @@ class BaseDiskFile(object):
|
||||
data_file,
|
||||
"Hash of name in metadata does not match directory name")
|
||||
|
||||
def _verify_data_file(self, data_file, fp):
|
||||
def _verify_data_file(self, data_file, fp, current_time):
|
||||
"""
|
||||
Verify the metadata's name value matches what we think the object is
|
||||
named.
|
||||
@ -2362,6 +2365,7 @@ class BaseDiskFile(object):
|
||||
occur
|
||||
:param fp: open file pointer so that we can `fstat()` the file to
|
||||
verify the on-disk size with Content-Length metadata value
|
||||
:param current_time: Unix time used in checking expiration
|
||||
:raises DiskFileCollision: if the metadata stored name does not match
|
||||
the referenced name of the file
|
||||
:raises DiskFileExpired: if the object has expired
|
||||
@ -2392,7 +2396,9 @@ class BaseDiskFile(object):
|
||||
data_file, "bad metadata x-delete-at value %s" % (
|
||||
self._metadata['X-Delete-At']))
|
||||
else:
|
||||
if x_delete_at <= time.time() and not self._open_expired:
|
||||
if current_time is None:
|
||||
current_time = time.time()
|
||||
if x_delete_at <= current_time and not self._open_expired:
|
||||
raise DiskFileExpired(metadata=self._metadata)
|
||||
try:
|
||||
metadata_size = int(self._metadata['Content-Length'])
|
||||
@ -2466,7 +2472,7 @@ class BaseDiskFile(object):
|
||||
ctypefile_metadata.get('Content-Type-Timestamp')
|
||||
|
||||
def _construct_from_data_file(self, data_file, meta_file, ctype_file,
|
||||
modernize=False,
|
||||
current_time, modernize=False,
|
||||
**kwargs):
|
||||
"""
|
||||
Open the `.data` file to fetch its metadata, and fetch the metadata
|
||||
@ -2477,6 +2483,7 @@ class BaseDiskFile(object):
|
||||
:param meta_file: on-disk fast-POST `.meta` file being considered
|
||||
:param ctype_file: on-disk fast-POST `.meta` file being considered that
|
||||
contains content-type and content-type timestamp
|
||||
:param current_time: Unix time used in checking expiration
|
||||
:param modernize: whether to update the on-disk files to the newest
|
||||
format
|
||||
:returns: an opened data file pointer
|
||||
@ -2524,7 +2531,7 @@ class BaseDiskFile(object):
|
||||
# to us
|
||||
self._name = self._metadata['name']
|
||||
self._verify_name_matches_hash(data_file)
|
||||
self._verify_data_file(data_file, fp)
|
||||
self._verify_data_file(data_file, fp, current_time)
|
||||
return fp
|
||||
|
||||
def get_metafile_metadata(self):
|
||||
@ -2571,16 +2578,18 @@ class BaseDiskFile(object):
|
||||
raise DiskFileNotOpen()
|
||||
return self._metadata
|
||||
|
||||
def read_metadata(self):
|
||||
def read_metadata(self, current_time=None):
|
||||
"""
|
||||
Return the metadata for an object without requiring the caller to open
|
||||
the object first.
|
||||
|
||||
:param current_time: Unix time used in checking expiration. If not
|
||||
present, the current time will be used.
|
||||
:returns: metadata dictionary for an object
|
||||
:raises DiskFileError: this implementation will raise the same
|
||||
errors as the `open()` method.
|
||||
"""
|
||||
with self.open():
|
||||
with self.open(current_time=current_time):
|
||||
return self.get_metadata()
|
||||
|
||||
def reader(self, keep_cache=False,
|
||||
|
@ -273,11 +273,14 @@ class DiskFile(object):
|
||||
self._filesystem = fs
|
||||
self.fragments = None
|
||||
|
||||
def open(self, modernize=False):
|
||||
def open(self, modernize=False, current_time=None):
|
||||
"""
|
||||
Open the file and read the metadata.
|
||||
|
||||
This method must populate the _metadata attribute.
|
||||
|
||||
:param current_time: Unix time used in checking expiration. If not
|
||||
present, the current time will be used.
|
||||
:raises DiskFileCollision: on name mis-match with metadata
|
||||
:raises DiskFileDeleted: if it does not exist, or a tombstone is
|
||||
present
|
||||
@ -287,7 +290,7 @@ class DiskFile(object):
|
||||
fp, self._metadata = self._filesystem.get_object(self._name)
|
||||
if fp is None:
|
||||
raise DiskFileDeleted()
|
||||
self._fp = self._verify_data_file(fp)
|
||||
self._fp = self._verify_data_file(fp, current_time)
|
||||
self._metadata = self._metadata or {}
|
||||
return self
|
||||
|
||||
@ -313,7 +316,7 @@ class DiskFile(object):
|
||||
self._filesystem.del_object(name)
|
||||
return DiskFileQuarantined(msg)
|
||||
|
||||
def _verify_data_file(self, fp):
|
||||
def _verify_data_file(self, fp, current_time):
|
||||
"""
|
||||
Verify the metadata's name value matches what we think the object is
|
||||
named.
|
||||
@ -344,7 +347,9 @@ class DiskFile(object):
|
||||
self._name, "bad metadata x-delete-at value %s" % (
|
||||
self._metadata['X-Delete-At']))
|
||||
else:
|
||||
if x_delete_at <= time.time():
|
||||
if current_time is None:
|
||||
current_time = time.time()
|
||||
if x_delete_at <= current_time:
|
||||
raise DiskFileNotExist('Expired')
|
||||
try:
|
||||
metadata_size = int(self._metadata['Content-Length'])
|
||||
@ -381,13 +386,15 @@ class DiskFile(object):
|
||||
raise DiskFileNotOpen()
|
||||
return self._metadata
|
||||
|
||||
def read_metadata(self):
|
||||
def read_metadata(self, current_time=None):
|
||||
"""
|
||||
Return the metadata for an object.
|
||||
|
||||
:param current_time: Unix time used in checking expiration. If not
|
||||
present, the current time will be used.
|
||||
:returns: metadata dictionary for an object
|
||||
"""
|
||||
with self.open():
|
||||
with self.open(current_time=current_time):
|
||||
return self.get_metadata()
|
||||
|
||||
def reader(self, keep_cache=False):
|
||||
|
@ -35,7 +35,7 @@ from swift.common.utils import public, get_logger, \
|
||||
normalize_delete_at_timestamp, get_log_line, Timestamp, \
|
||||
get_expirer_container, parse_mime_headers, \
|
||||
iter_multipart_mime_documents, extract_swift_bytes, safe_json_loads, \
|
||||
config_auto_int_value, split_path, get_redirect_data
|
||||
config_auto_int_value, split_path, get_redirect_data, normalize_timestamp
|
||||
from swift.common.bufferedhttp import http_connect
|
||||
from swift.common.constraints import check_object_creation, \
|
||||
valid_timestamp, check_utf8
|
||||
@ -591,7 +591,7 @@ class ObjectController(BaseStorageServer):
|
||||
get_name_and_placement(request, 5, 5, True)
|
||||
req_timestamp = valid_timestamp(request)
|
||||
new_delete_at = int(request.headers.get('X-Delete-At') or 0)
|
||||
if new_delete_at and new_delete_at < time.time():
|
||||
if new_delete_at and new_delete_at < req_timestamp:
|
||||
return HTTPBadRequest(body='X-Delete-At in past', request=request,
|
||||
content_type='text/plain')
|
||||
next_part_power = request.headers.get('X-Backend-Next-Part-Power')
|
||||
@ -604,7 +604,7 @@ class ObjectController(BaseStorageServer):
|
||||
except DiskFileDeviceUnavailable:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
try:
|
||||
orig_metadata = disk_file.read_metadata()
|
||||
orig_metadata = disk_file.read_metadata(current_time=req_timestamp)
|
||||
except DiskFileXattrNotSupported:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
except (DiskFileNotExist, DiskFileQuarantined):
|
||||
@ -766,7 +766,7 @@ class ObjectController(BaseStorageServer):
|
||||
except DiskFileDeviceUnavailable:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
try:
|
||||
orig_metadata = disk_file.read_metadata()
|
||||
orig_metadata = disk_file.read_metadata(current_time=req_timestamp)
|
||||
orig_timestamp = disk_file.data_timestamp
|
||||
except DiskFileXattrNotSupported:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
@ -954,6 +954,9 @@ class ObjectController(BaseStorageServer):
|
||||
"""Handle HTTP GET requests for the Swift Object Server."""
|
||||
device, partition, account, container, obj, policy = \
|
||||
get_name_and_placement(request, 5, 5, True)
|
||||
request.headers.setdefault('X-Timestamp',
|
||||
normalize_timestamp(time.time()))
|
||||
req_timestamp = valid_timestamp(request)
|
||||
frag_prefs = safe_json_loads(
|
||||
request.headers.get('X-Backend-Fragment-Preferences'))
|
||||
try:
|
||||
@ -965,7 +968,7 @@ class ObjectController(BaseStorageServer):
|
||||
except DiskFileDeviceUnavailable:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
try:
|
||||
with disk_file.open():
|
||||
with disk_file.open(current_time=req_timestamp):
|
||||
metadata = disk_file.get_metadata()
|
||||
obj_size = int(metadata['Content-Length'])
|
||||
file_x_ts = Timestamp(metadata['X-Timestamp'])
|
||||
@ -1018,6 +1021,9 @@ class ObjectController(BaseStorageServer):
|
||||
"""Handle HTTP HEAD requests for the Swift Object Server."""
|
||||
device, partition, account, container, obj, policy = \
|
||||
get_name_and_placement(request, 5, 5, True)
|
||||
request.headers.setdefault('X-Timestamp',
|
||||
normalize_timestamp(time.time()))
|
||||
req_timestamp = valid_timestamp(request)
|
||||
frag_prefs = safe_json_loads(
|
||||
request.headers.get('X-Backend-Fragment-Preferences'))
|
||||
try:
|
||||
@ -1029,7 +1035,7 @@ class ObjectController(BaseStorageServer):
|
||||
except DiskFileDeviceUnavailable:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
try:
|
||||
metadata = disk_file.read_metadata()
|
||||
metadata = disk_file.read_metadata(current_time=req_timestamp)
|
||||
except DiskFileXattrNotSupported:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
except (DiskFileNotExist, DiskFileQuarantined) as e:
|
||||
@ -1083,7 +1089,7 @@ class ObjectController(BaseStorageServer):
|
||||
except DiskFileDeviceUnavailable:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
try:
|
||||
orig_metadata = disk_file.read_metadata()
|
||||
orig_metadata = disk_file.read_metadata(current_time=req_timestamp)
|
||||
except DiskFileXattrNotSupported:
|
||||
return HTTPInsufficientStorage(drive=device, request=request)
|
||||
except DiskFileExpired as e:
|
||||
|
@ -1338,7 +1338,6 @@ class TestObjectController(unittest.TestCase):
|
||||
inital_put = next(self.ts)
|
||||
put_before_expire = next(self.ts)
|
||||
delete_at_timestamp = int(next(self.ts))
|
||||
time_after_expire = next(self.ts)
|
||||
put_after_expire = next(self.ts)
|
||||
delete_at_container = str(
|
||||
delete_at_timestamp /
|
||||
@ -1363,9 +1362,7 @@ class TestObjectController(unittest.TestCase):
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'If-None-Match': '*'})
|
||||
req.body = 'TEST'
|
||||
with mock.patch("swift.obj.server.time.time",
|
||||
lambda: float(put_before_expire.normal)):
|
||||
resp = req.get_response(self.object_controller)
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 412)
|
||||
|
||||
# PUT again after object has expired should succeed
|
||||
@ -1376,9 +1373,7 @@ class TestObjectController(unittest.TestCase):
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'If-None-Match': '*'})
|
||||
req.body = 'TEST'
|
||||
with mock.patch("swift.obj.server.time.time",
|
||||
lambda: float(time_after_expire.normal)):
|
||||
resp = req.get_response(self.object_controller)
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
|
||||
def test_PUT_common(self):
|
||||
@ -3880,7 +3875,7 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
disk_file = self.df_mgr.get_diskfile('sda1', 'p', 'a', 'c', 'o',
|
||||
policy=POLICIES.legacy)
|
||||
disk_file.open()
|
||||
disk_file.open(timestamp)
|
||||
file_name = os.path.basename(disk_file._data_file)
|
||||
with open(disk_file._data_file) as fp:
|
||||
metadata = diskfile.read_metadata(fp)
|
||||
@ -3909,7 +3904,7 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
disk_file = self.df_mgr.get_diskfile('sda1', 'p', 'a', 'c', 'o',
|
||||
policy=POLICIES.legacy)
|
||||
disk_file.open()
|
||||
disk_file.open(timestamp)
|
||||
file_name = os.path.basename(disk_file._data_file)
|
||||
etag = md5()
|
||||
etag.update('VERIF')
|
||||
@ -4624,8 +4619,10 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertEqual(headers[:len(exp)], exp)
|
||||
sock = connect_tcp(('localhost', port))
|
||||
fd = sock.makefile()
|
||||
fd.write('GET /sda1/p/a/c/o HTTP/1.1\r\nHost: localhost\r\n'
|
||||
'Connection: close\r\n\r\n')
|
||||
fd.write('GET /sda1/p/a/c/o HTTP/1.1\r\n'
|
||||
'Host: localhost\r\n'
|
||||
'X-Timestamp: %s\r\n'
|
||||
'Connection: close\r\n\r\n' % normalize_timestamp(2.0))
|
||||
fd.flush()
|
||||
headers = readuntil2crlfs(fd)
|
||||
exp = 'HTTP/1.1 200'
|
||||
@ -6090,8 +6087,7 @@ class TestObjectController(unittest.TestCase):
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'X-Timestamp': normalize_timestamp(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, 200)
|
||||
|
||||
# It expires in the past, so it's not accessible via GET...
|
||||
@ -6100,29 +6096,25 @@ class TestObjectController(unittest.TestCase):
|
||||
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, 404)
|
||||
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
||||
utils.Timestamp(now))
|
||||
|
||||
with mock.patch('swift.obj.server.time.time',
|
||||
return_value=delete_at_timestamp + 1):
|
||||
# ...unless X-Backend-Replication is sent
|
||||
expected = {
|
||||
'GET': 'TEST',
|
||||
'HEAD': '',
|
||||
}
|
||||
for meth, expected_body in expected.items():
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', method=meth,
|
||||
headers={'X-Timestamp':
|
||||
normalize_timestamp(delete_at_timestamp + 1),
|
||||
'X-Backend-Replication': 'True'})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(expected_body, resp.body)
|
||||
# ...unless X-Backend-Replication is sent
|
||||
expected = {
|
||||
'GET': 'TEST',
|
||||
'HEAD': '',
|
||||
}
|
||||
for meth, expected_body in expected.items():
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', method=meth,
|
||||
headers={'X-Timestamp':
|
||||
normalize_timestamp(delete_at_timestamp + 1),
|
||||
'X-Backend-Replication': 'True'})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(expected_body, resp.body)
|
||||
|
||||
def test_HEAD_but_expired(self):
|
||||
# We have an object that expires in the future
|
||||
@ -6148,8 +6140,7 @@ class TestObjectController(unittest.TestCase):
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'HEAD'},
|
||||
headers={'X-Timestamp': normalize_timestamp(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, 200)
|
||||
|
||||
# It's not accessible now since it expires in the past
|
||||
@ -6158,9 +6149,7 @@ class TestObjectController(unittest.TestCase):
|
||||
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)
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
self.assertEqual(resp.headers['X-Backend-Timestamp'],
|
||||
utils.Timestamp(now))
|
||||
@ -6195,8 +6184,7 @@ class TestObjectController(unittest.TestCase):
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'POST'},
|
||||
headers={'X-Timestamp': normalize_timestamp(the_time)})
|
||||
with mock.patch('swift.obj.server.time.time', return_value=the_time):
|
||||
resp = req.get_response(self.object_controller)
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 202)
|
||||
|
||||
# You cannot POST to an expired object
|
||||
@ -6207,8 +6195,7 @@ class TestObjectController(unittest.TestCase):
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'POST'},
|
||||
headers={'X-Timestamp': normalize_timestamp(the_time)})
|
||||
with mock.patch('swift.obj.server.time.time', return_value=the_time):
|
||||
resp = req.get_response(self.object_controller)
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
|
||||
def test_DELETE_can_skip_updating_expirer_queue(self):
|
||||
@ -6331,24 +6318,21 @@ class TestObjectController(unittest.TestCase):
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
|
||||
orig_time = object_server.time.time
|
||||
try:
|
||||
t = test_time + 100
|
||||
object_server.time.time = lambda: float(t)
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': normalize_timestamp(time())})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
finally:
|
||||
object_server.time.time = orig_time
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': normalize_timestamp(
|
||||
delete_at_timestamp + 1)})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
|
||||
def test_DELETE_if_delete_at_expired_still_deletes(self):
|
||||
test_time = time() + 10
|
||||
test_timestamp = normalize_timestamp(test_time)
|
||||
delete_at_time = int(test_time + 10)
|
||||
delete_at_timestamp = str(delete_at_time)
|
||||
expired_time = delete_at_time + 1
|
||||
expired_timestamp = normalize_timestamp(expired_time)
|
||||
delete_at_container = str(
|
||||
delete_at_time /
|
||||
self.object_controller.expiring_objects_container_divisor *
|
||||
@ -6379,73 +6363,71 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertTrue(os.path.isfile(objfile))
|
||||
|
||||
# move time past expiry
|
||||
with mock.patch('swift.obj.diskfile.time') as mock_time:
|
||||
mock_time.time.return_value = test_time + 100
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'X-Timestamp': test_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
# request will 404
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
# but file still exists
|
||||
self.assertTrue(os.path.isfile(objfile))
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'X-Timestamp': expired_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
# request will 404
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
# but file still exists
|
||||
self.assertTrue(os.path.isfile(objfile))
|
||||
|
||||
# make the x-if-delete-at with some wrong bits
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': int(time() + 1)})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 412)
|
||||
self.assertTrue(os.path.isfile(objfile))
|
||||
# make the x-if-delete-at with some wrong bits
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': int(delete_at_time + 1)})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 412)
|
||||
self.assertTrue(os.path.isfile(objfile))
|
||||
|
||||
# make the x-if-delete-at with all the right bits
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 204)
|
||||
self.assertFalse(os.path.isfile(objfile))
|
||||
# make the x-if-delete-at with all the right bits
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 204)
|
||||
self.assertFalse(os.path.isfile(objfile))
|
||||
|
||||
# make the x-if-delete-at with all the right bits (again)
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 409)
|
||||
self.assertFalse(os.path.isfile(objfile))
|
||||
# make the x-if-delete-at with all the right bits (again)
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 409)
|
||||
self.assertFalse(os.path.isfile(objfile))
|
||||
|
||||
# overwrite with new content
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
||||
headers={
|
||||
'X-Timestamp': str(test_time + 100),
|
||||
'Content-Length': '0',
|
||||
'Content-Type': 'application/octet-stream'})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 201, resp.body)
|
||||
# overwrite with new content
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
|
||||
headers={
|
||||
'X-Timestamp': str(test_time + 100),
|
||||
'Content-Length': '0',
|
||||
'Content-Type': 'application/octet-stream'})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 201, resp.body)
|
||||
|
||||
# simulate processing a stale expirer queue entry
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 409)
|
||||
# simulate processing a stale expirer queue entry
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 409)
|
||||
|
||||
# make the x-if-delete-at for some not found
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o-not-found',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
# make the x-if-delete-at for some not found
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o-not-found',
|
||||
environ={'REQUEST_METHOD': 'DELETE'},
|
||||
headers={'X-Timestamp': delete_at_timestamp,
|
||||
'X-If-Delete-At': delete_at_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
|
||||
def test_DELETE_if_delete_at(self):
|
||||
test_time = time() + 10000
|
||||
@ -6755,6 +6737,38 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
self.assertTrue('X-Delete-At in past' in resp.body)
|
||||
|
||||
def test_POST_delete_at_in_past_with_skewed_clock(self):
|
||||
proxy_server_put_time = 1000
|
||||
proxy_server_post_time = 1001
|
||||
delete_at = 1050
|
||||
obj_server_put_time = 1100
|
||||
obj_server_post_time = 1101
|
||||
|
||||
# test setup: make an object for us to POST to
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'PUT'},
|
||||
headers={'X-Timestamp': normalize_timestamp(proxy_server_put_time),
|
||||
'Content-Length': '4',
|
||||
'Content-Type': 'application/octet-stream'})
|
||||
req.body = 'TEST'
|
||||
with mock.patch('swift.obj.server.time.time',
|
||||
return_value=obj_server_put_time):
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
|
||||
# then POST to it
|
||||
req = Request.blank(
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'POST'},
|
||||
headers={'X-Timestamp':
|
||||
normalize_timestamp(proxy_server_post_time),
|
||||
'X-Delete-At': str(delete_at)})
|
||||
with mock.patch('swift.obj.server.time.time',
|
||||
return_value=obj_server_post_time):
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 202)
|
||||
|
||||
def test_REPLICATE_works(self):
|
||||
|
||||
def fake_get_hashes(*args, **kwargs):
|
||||
@ -7223,14 +7237,12 @@ class TestObjectController(unittest.TestCase):
|
||||
'/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'HEAD', 'REMOTE_ADDR': '1.2.3.4'})
|
||||
self.object_controller.logger = self.logger
|
||||
with mock.patch(
|
||||
'time.gmtime', mock.MagicMock(side_effect=[gmtime(10001.0)])):
|
||||
with mock.patch(
|
||||
with mock.patch('time.gmtime', side_effect=[gmtime(10001.0)]), \
|
||||
mock.patch(
|
||||
'time.time',
|
||||
mock.MagicMock(side_effect=[10000.0, 10001.0, 10002.0])):
|
||||
with mock.patch(
|
||||
'os.getpid', mock.MagicMock(return_value=1234)):
|
||||
req.get_response(self.object_controller)
|
||||
side_effect=[10000.0, 10000.0, 10001.0, 10002.0]), \
|
||||
mock.patch('os.getpid', return_value=1234):
|
||||
req.get_response(self.object_controller)
|
||||
self.assertEqual(
|
||||
self.logger.get_lines_for_level('info'),
|
||||
['1.2.3.4 - - [01/Jan/1970:02:46:41 +0000] "HEAD /sda1/p/a/c/o" '
|
||||
@ -7319,6 +7331,7 @@ class TestObjectController(unittest.TestCase):
|
||||
existing_timestamp = normalize_timestamp(time())
|
||||
delete_timestamp = normalize_timestamp(time() + 1)
|
||||
put_timestamp = normalize_timestamp(time() + 2)
|
||||
head_timestamp = normalize_timestamp(time() + 3)
|
||||
|
||||
# make a .ts
|
||||
req = Request.blank(
|
||||
@ -7355,7 +7368,8 @@ class TestObjectController(unittest.TestCase):
|
||||
self.assertFalse(os.path.exists(qdir))
|
||||
|
||||
req = Request.blank('/sda1/p/a/c/o',
|
||||
environ={'REQUEST_METHOD': 'HEAD'})
|
||||
environ={'REQUEST_METHOD': 'HEAD'},
|
||||
headers={'X-Timestamp': head_timestamp})
|
||||
resp = req.get_response(self.object_controller)
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(resp.headers['X-Timestamp'], put_timestamp)
|
||||
|
Loading…
x
Reference in New Issue
Block a user