diff --git a/swift/obj/server.py b/swift/obj/server.py index e04d662f83..30c147e6c3 100644 --- a/swift/obj/server.py +++ b/swift/obj/server.py @@ -389,14 +389,26 @@ class ObjectController(BaseStorageServer): 'x-trans-id': headers_in.get('x-trans-id', '-'), 'referer': request.as_referer()}) if op != 'DELETE': - delete_at_container = headers_in.get('X-Delete-At-Container', None) - if not delete_at_container: + hosts = headers_in.get('X-Delete-At-Host', None) + if hosts is None: # If header is missing, no update needed as sufficient other # object servers should perform the required update. return - + delete_at_container = headers_in.get('X-Delete-At-Container', None) + if not delete_at_container: + # older proxy servers did not send X-Delete-At-Container so for + # backwards compatibility calculate the value here, but also + # log a warning because this is prone to inconsistent + # expiring_objects_container_divisor configurations. + # See https://bugs.launchpad.net/swift/+bug/1187200 + self.logger.warning( + 'X-Delete-At-Container header must be specified for ' + 'expiring objects background %s to work properly. Making ' + 'best guess as to the container name for now.' % op) + delete_at_container = get_expirer_container( + delete_at, self.expiring_objects_container_divisor, + account, container, obj) partition = headers_in.get('X-Delete-At-Partition', None) - hosts = headers_in.get('X-Delete-At-Host', '') contdevices = headers_in.get('X-Delete-At-Device', '') updates = [upd for upd in zip((h.strip() for h in hosts.split(',')), diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py index ccff2a40e1..ef5c63beea 100644 --- a/test/unit/obj/test_server.py +++ b/test/unit/obj/test_server.py @@ -5597,8 +5597,116 @@ class TestObjectController(unittest.TestCase): 'X-Backend-Storage-Policy-Index': int(policy)}) self.object_controller.delete_at_update('PUT', 2, 'a', 'c', 'o', req, 'sda1', policy) + self.assertEqual( + self.logger.get_lines_for_level('warning'), + ['X-Delete-At-Container header must be specified for expiring ' + 'objects background PUT to work properly. Making best guess as ' + 'to the container name for now.']) + self.assertEqual( + given_args, [ + 'PUT', '.expiring_objects', '0000000000', '0000000002-a/c/o', + '127.0.0.1:1234', + '3', 'sdc1', HeaderKeyDict({ + # the .expiring_objects account is always policy-0 + 'X-Backend-Storage-Policy-Index': 0, + 'x-size': '0', + 'x-etag': 'd41d8cd98f00b204e9800998ecf8427e', + 'x-content-type': 'text/plain', + 'x-timestamp': utils.Timestamp('1').internal, + 'x-trans-id': '1234', + 'referer': 'PUT http://localhost/v1/a/c/o'}), + 'sda1', policy]) + + def test_delete_at_update_put_with_info_but_missing_host(self): + # Same as test_delete_at_update_put_with_info, but just + # missing the X-Delete-At-Host header. + policy = random.choice(list(POLICIES)) + given_args = [] + + def fake_async_update(*args): + given_args.extend(args) + + self.object_controller.async_update = fake_async_update + self.object_controller.logger = self.logger + req = Request.blank( + '/v1/a/c/o', + environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': 1, + 'X-Trans-Id': '1234', + 'X-Delete-At-Container': '0', + 'X-Delete-At-Partition': '3', + 'X-Delete-At-Device': 'sdc1', + 'X-Backend-Storage-Policy-Index': int(policy)}) + self.object_controller.delete_at_update('PUT', 2, 'a', 'c', 'o', + req, 'sda1', policy) + self.assertFalse(self.logger.get_lines_for_level('warning')) self.assertEqual(given_args, []) + def test_delete_at_update_put_with_info_but_empty_host(self): + # Same as test_delete_at_update_put_with_info, but empty + # X-Delete-At-Host header and no X-Delete-At-Partition nor + # X-Delete-At-Device. + policy = random.choice(list(POLICIES)) + given_args = [] + + def fake_async_update(*args): + given_args.extend(args) + + self.object_controller.async_update = fake_async_update + self.object_controller.logger = self.logger + req = Request.blank( + '/v1/a/c/o', + environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': 1, + 'X-Trans-Id': '1234', + 'X-Delete-At-Container': '0', + 'X-Delete-At-Host': '', + 'X-Backend-Storage-Policy-Index': int(policy)}) + self.object_controller.delete_at_update('PUT', 2, 'a', 'c', 'o', + req, 'sda1', policy) + self.assertFalse(self.logger.get_lines_for_level('warning')) + self.assertEqual( + given_args, [ + 'PUT', '.expiring_objects', '0000000000', '0000000002-a/c/o', + None, + None, None, HeaderKeyDict({ + # the .expiring_objects account is always policy-0 + 'X-Backend-Storage-Policy-Index': 0, + 'x-size': '0', + 'x-etag': 'd41d8cd98f00b204e9800998ecf8427e', + 'x-content-type': 'text/plain', + 'x-timestamp': utils.Timestamp('1').internal, + 'x-trans-id': '1234', + 'referer': 'PUT http://localhost/v1/a/c/o'}), + 'sda1', policy]) + + def test_delete_at_update_delete(self): + policy = random.choice(list(POLICIES)) + given_args = [] + + def fake_async_update(*args): + given_args.extend(args) + + self.object_controller.async_update = fake_async_update + req = Request.blank( + '/v1/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': 1, + 'X-Trans-Id': '1234', + 'X-Backend-Storage-Policy-Index': int(policy)}) + self.object_controller.delete_at_update('DELETE', 2, 'a', 'c', 'o', + req, 'sda1', policy) + self.assertEqual( + given_args, [ + 'DELETE', '.expiring_objects', '0000000000', + '0000000002-a/c/o', None, None, + None, HeaderKeyDict({ + 'X-Backend-Storage-Policy-Index': 0, + 'x-timestamp': utils.Timestamp('1').internal, + 'x-trans-id': '1234', + 'referer': 'DELETE http://localhost/v1/a/c/o'}), + 'sda1', policy]) + def test_delete_backend_replication(self): # If X-Backend-Replication: True delete_at_update should completely # short-circuit. @@ -6249,6 +6357,7 @@ class TestObjectController(unittest.TestCase): headers={'X-Timestamp': normalize_timestamp(put_time), 'X-Delete-At': str(delete_at_timestamp_1), 'X-Delete-At-Container': delete_at_container_1, + 'X-Delete-At-Host': '1.2.3.4', 'Content-Length': '4', 'Content-Type': 'application/octet-stream'}) req.body = 'TEST' @@ -6266,6 +6375,7 @@ class TestObjectController(unittest.TestCase): 'X-Backend-Clean-Expiring-Object-Queue': 'false', 'X-Delete-At': str(delete_at_timestamp_2), 'X-Delete-At-Container': delete_at_container_2, + 'X-Delete-At-Host': '1.2.3.4', 'Content-Length': '9', 'Content-Type': 'application/octet-stream'}) req.body = 'new stuff'