Fix delete versioning objects when previous is expired

When deleteing versioned objects proxy will try to restore the previous
copy. The COPY request will fail if the previous version is expired but
not handled by object-expirer.

This patch checks COPY respones on the previous copy, if it's
HTTP_NOT_FOUND(mostly because it's expired) proxy will try to restore
with the version before previous.

Closes-Bug #1308446
Change-Id: I17f049ea3ef62723effae8086ec427f6e151cd9c
This commit is contained in:
Yuan Zhou 2014-04-17 15:39:50 +08:00
parent c5013783ac
commit 84a1e17f20
2 changed files with 71 additions and 8 deletions

View File

@ -799,11 +799,11 @@ class ObjectController(Controller):
lcontainer = object_versions.split('/')[0] lcontainer = object_versions.split('/')[0]
prefix_len = '%03x' % len(self.object_name) prefix_len = '%03x' % len(self.object_name)
lprefix = prefix_len + self.object_name + '/' lprefix = prefix_len + self.object_name + '/'
last_item = None item_list = []
try: try:
for last_item in self._listing_iter(lcontainer, lprefix, for _item in self._listing_iter(lcontainer, lprefix,
req.environ): req.environ):
pass item_list.append(_item)
except ListingIterNotFound: except ListingIterNotFound:
# no worries, last_item is None # no worries, last_item is None
pass pass
@ -811,15 +811,19 @@ class ObjectController(Controller):
return err.aresp return err.aresp
except ListingIterError: except ListingIterError:
return HTTPServerError(request=req) return HTTPServerError(request=req)
if last_item:
while len(item_list) > 0:
previous_version = item_list.pop()
# there are older versions so copy the previous version to the # there are older versions so copy the previous version to the
# current object and delete the previous version # current object and delete the previous version
orig_container = self.container_name orig_container = self.container_name
orig_obj = self.object_name orig_obj = self.object_name
self.container_name = lcontainer self.container_name = lcontainer
self.object_name = last_item['name'].encode('utf-8') self.object_name = previous_version['name'].encode('utf-8')
copy_path = '/v1/' + self.account_name + '/' + \ copy_path = '/v1/' + self.account_name + '/' + \
self.container_name + '/' + self.object_name self.container_name + '/' + self.object_name
copy_headers = {'X-Newest': 'True', copy_headers = {'X-Newest': 'True',
'Destination': orig_container + '/' + orig_obj 'Destination': orig_container + '/' + orig_obj
} }
@ -829,6 +833,11 @@ class ObjectController(Controller):
creq = Request.blank(copy_path, headers=copy_headers, creq = Request.blank(copy_path, headers=copy_headers,
environ=copy_environ) environ=copy_environ)
copy_resp = self.COPY(creq) copy_resp = self.COPY(creq)
if copy_resp.status_int == HTTP_NOT_FOUND:
# the version isn't there so we'll try with previous
self.container_name = orig_container
self.object_name = orig_obj
continue
if is_client_error(copy_resp.status_int): if is_client_error(copy_resp.status_int):
# some user error, maybe permissions # some user error, maybe permissions
return HTTPPreconditionFailed(request=req) return HTTPPreconditionFailed(request=req)
@ -837,7 +846,7 @@ class ObjectController(Controller):
return HTTPServiceUnavailable(request=req) return HTTPServiceUnavailable(request=req)
# reset these because the COPY changed them # reset these because the COPY changed them
self.container_name = lcontainer self.container_name = lcontainer
self.object_name = last_item['name'].encode('utf-8') self.object_name = previous_version['name'].encode('utf-8')
new_del_req = Request.blank(copy_path, environ=req.environ) new_del_req = Request.blank(copy_path, environ=req.environ)
container_info = self.container_info( container_info = self.container_info(
self.account_name, self.container_name, req) self.account_name, self.container_name, req)
@ -854,6 +863,7 @@ class ObjectController(Controller):
# remove 'X-If-Delete-At', since it is not for the older copy # remove 'X-If-Delete-At', since it is not for the older copy
if 'X-If-Delete-At' in req.headers: if 'X-If-Delete-At' in req.headers:
del req.headers['X-If-Delete-At'] del req.headers['X-If-Delete-At']
break
if 'swift.authorize' in req.environ: if 'swift.authorize' in req.environ:
aresp = req.environ['swift.authorize'](req) aresp = req.environ['swift.authorize'](req)
if aresp: if aresp:

View File

@ -1511,7 +1511,7 @@ class TestObjectController(unittest.TestCase):
# HEAD HEAD GET GET HEAD GET GET GET PUT PUT # HEAD HEAD GET GET HEAD GET GET GET PUT PUT
# PUT DEL DEL DEL # PUT DEL DEL DEL
set_http_connect(200, 200, 200, 200, 200, 200, 200, 200, 201, 201, set_http_connect(200, 200, 200, 200, 200, 200, 200, 200, 201, 201,
201, 200, 200, 200, 201, 204, 204, 204,
give_connect=test_connect, give_connect=test_connect,
body_iter=body_iter, body_iter=body_iter,
headers={'x-versions-location': 'foo'}) headers={'x-versions-location': 'foo'})
@ -1523,6 +1523,59 @@ class TestObjectController(unittest.TestCase):
controller.DELETE(req) controller.DELETE(req)
self.assertEquals(test_errors, []) self.assertEquals(test_errors, [])
@patch_policies([
StoragePolicy(0, 'zero', False, object_ring=FakeRing()),
StoragePolicy(1, 'one', True, object_ring=FakeRing())
])
def test_DELETE_on_expired_versioned_object(self):
methods = set()
def test_connect(ipaddr, port, device, partition, method, path,
headers=None, query_string=None):
methods.add((method, path))
def fake_container_info(account, container, req):
return {'status': 200, 'sync_key': None,
'meta': {}, 'cors': {'allow_origin': None,
'expose_headers': None,
'max_age': None},
'sysmeta': {}, 'read_acl': None, 'object_count': None,
'write_acl': None, 'versions': 'foo',
'partition': 1, 'bytes': None, 'storage_policy': '1',
'nodes': [{'zone': 0, 'ip': '10.0.0.0', 'region': 0,
'id': 0, 'device': 'sda', 'port': 1000},
{'zone': 1, 'ip': '10.0.0.1', 'region': 1,
'id': 1, 'device': 'sdb', 'port': 1001},
{'zone': 2, 'ip': '10.0.0.2', 'region': 0,
'id': 2, 'device': 'sdc', 'port': 1002}]}
def fake_list_iter(container, prefix, env):
object_list = [{'name': '1'}, {'name': '2'}, {'name': '3'}]
for obj in object_list:
yield obj
with save_globals():
controller = proxy_server.ObjectController(self.app,
'a', 'c', 'o')
controller.container_info = fake_container_info
controller._listing_iter = fake_list_iter
set_http_connect(404, 404, 404, # get for the previous version
200, 200, 200, # get for the pre-previous
201, 201, 201, # put move the pre-previous
204, 204, 204, # delete for the pre-previous
give_connect=test_connect)
req = Request.blank('/v1/a/c/o',
environ={'REQUEST_METHOD': 'DELETE'})
self.app.memcache.store = {}
self.app.update_request(req)
controller.DELETE(req)
exp_methods = [('GET', '/a/foo/3'),
('GET', '/a/foo/2'),
('PUT', '/a/c/o'),
('DELETE', '/a/foo/2')]
self.assertEquals(set(exp_methods), (methods))
def test_PUT_auto_content_type(self): def test_PUT_auto_content_type(self):
with save_globals(): with save_globals():
controller = proxy_server.ObjectController(self.app, 'account', controller = proxy_server.ObjectController(self.app, 'account',