diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index c43210e7e7..d69f866b9c 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -666,6 +666,27 @@ X-Fresh-Metadata: in: header required: false type: boolean +X-History-Location: + description: | + The URL-encoded UTF-8 representation of the container that stores + previous versions of objects. If neither this nor ``X-Versions-Location`` + is set, versioning is disabled for this container. ``X-History-Location`` + and ``X-Versions-Location`` cannot both be set at the same time. For more + information about object versioning, see `Object versioning + `_. + in: header + required: false + type: string +X-History-Location_resp: + description: | + If present, this container has versioning enabled and the value + is the UTF-8 encoded name of another container. + For more information about object versioning, + see `Object versioning `_. + in: header + required: false + type: string X-Newest: description: | If set to true , Object Storage queries all @@ -728,9 +749,17 @@ X-Remove-Container-name: in: header required: false type: string +X-Remove-History-Location: + description: | + Set to any value to disable versioning. Note that this disables version + that was set via ``X-Versions-Location`` as well. + in: header + required: false + type: string X-Remove-Versions-Location: description: | - Set to any value to disable versioning. + Set to any value to disable versioning. Note that this disables version + that was set via ``X-History-Location`` as well. in: header required: false type: string @@ -801,10 +830,11 @@ X-Trans-Id-Extra: X-Versions-Location: description: | The URL-encoded UTF-8 representation of the container that stores - previous versions of objects. If not set, versioning is disabled - for this container. For more information about object versioning, - see `Object versioning `_. + previous versions of objects. If neither this nor ``X-History-Location`` + is set, versioning is disabled for this container. ``X-Versions-Location`` + and ``X-History-Location`` cannot both be set at the same time. For more + information about object versioning, see `Object versioning + `_. in: header required: false type: string @@ -818,26 +848,6 @@ X-Versions-Location_resp: in: header required: false type: string -X-Versions-Mode: - description: | - The versioning mode for this container. The value must be either - ``stack`` or ``history``. If not set, ``stack`` mode will be used. - This setting has no impact unless ``X-Versions-Location`` is set - for the container. For more information about object versioning, - see `Object versioning `_. - in: header - required: false - type: string -X-Versions-Mode_resp: - description: | - If set, this is the versioning mode for this container. The value is either - ``stack`` or ``history``. For more information about object versioning, - see `Object versioning `_. - in: header - required: false - type: string # variables in path account: diff --git a/api-ref/source/storage-container-services.inc b/api-ref/source/storage-container-services.inc index f45a03b265..07edbbb99b 100644 --- a/api-ref/source/storage-container-services.inc +++ b/api-ref/source/storage-container-services.inc @@ -85,7 +85,7 @@ Response Parameters - X-Container-Sync-Key: X-Container-Sync-Key_resp - X-Container-Sync-To: X-Container-Sync-To_resp - X-Versions-Location: X-Versions-Location_resp - - X-Versions-Mode: X-Versions-Mode_resp + - X-History-Location: X-History-Location_resp - X-Timestamp: X-Timestamp - X-Trans-Id: X-Trans-Id - Content_Type: Content-Type_listing_resp @@ -199,7 +199,7 @@ Request - X-Container-Sync-To: X-Container-Sync-To - X-Container-Sync-Key: X-Container-Sync-Key - X-Versions-Location: X-Versions-Location - - X-Versions-Mode: X-Versions-Mode + - X-History-Location: X-History-Location - X-Container-Meta-name: X-Container-Meta-name_req - X-Container-Meta-Access-Control-Allow-Origin: X-Container-Meta-Access-Control-Allow-Origin - X-Container-Meta-Access-Control-Max-Age: X-Container-Meta-Access-Control-Max-Age @@ -335,8 +335,9 @@ Request - X-Container-Sync-To: X-Container-Sync-To - X-Container-Sync-Key: X-Container-Sync-Key - X-Versions-Location: X-Versions-Location - - X-Versions-Mode: X-Versions-Mode + - X-History-Location: X-History-Location - X-Remove-Versions-Location: X-Remove-Versions-Location + - X-Remove-History-Location: X-Remove-History-Location - X-Container-Meta-name: X-Container-Meta-name_req - X-Container-Meta-Access-Control-Allow-Origin: X-Container-Meta-Access-Control-Allow-Origin - X-Container-Meta-Access-Control-Max-Age: X-Container-Meta-Access-Control-Max-Age @@ -440,7 +441,7 @@ Response Parameters - X-Trans-Id: X-Trans-Id - Content-Type: Content-Type_cud_resp - X-Versions-Location: X-Versions-Location_resp - - X-Versions-Mode: X-Versions-Mode_resp + - X-History-Location: X-History-Location_resp - X-Storage-Policy: X-Storage-Policy diff --git a/doc/source/api/object_versioning.rst b/doc/source/api/object_versioning.rst index 3b86d4ed69..1b78a132be 100644 --- a/doc/source/api/object_versioning.rst +++ b/doc/source/api/object_versioning.rst @@ -20,30 +20,37 @@ To allow object versioning within a cluster, the cloud provider should add the ``allow_versioned_writes`` option to ``true`` in the ``[filter:versioned_writes]`` section of the proxy-server configuration file. -The ``X-Versions-Location`` header defines the -container that holds the non-current versions of your objects. You -must UTF-8-encode and then URL-encode the container name before you -include it in the ``X-Versions-Location`` header. This header enables -object versioning for all objects in the container. With a comparable -``archive`` container in place, changes to objects in the ``current`` -container automatically create non-current versions in the ``archive`` -container. +To enable object versioning for a container, you must specify an "archive +container" that will retain non-current versions via either the +``X-Versions-Location`` or ``X-History-Location`` header. These two headers +enable two distinct modes of operation. Either mode may be used within a +cluster, but only one mode may be active for any given container. You must +UTF-8-encode and then URL-encode the container name before you include it in +the header. -The ``X-Versions-Mode`` header defines the behavior of ``DELETE`` requests to -objects in the versioned container. In the default ``stack`` mode, deleting an -object will restore the most-recent version from the ``archive`` container, -overwriting the curent version. Alternatively you may specify ``history`` -mode, where deleting an object will copy the current version to the -``archive`` then remove it from the ``current`` container. +For both modes, **PUT** requests will archive any pre-existing objects before +writing new data, and **GET** requests will serve the current version. **COPY** +requests behave like a **GET** followed by a **PUT**; that is, if the copy +*source* is in a versioned container then the current version will be copied, +and if the copy *destination* is in a versioned container then any pre-existing +object will be archived before writing new data. -Example Using ``stack`` Mode ----------------------------- +If object versioning was enabled using ``X-History-Location``, then object +**DELETE** requests will copy the current version to the archive container then +remove it from the versioned container. + +If object versioning was enabled using ``X-Versions-Location``, then object +**DELETE** requests will restore the most-recent version from the archive +container, overwriting the curent version. + +Example Using ``X-Versions-Location`` +------------------------------------- #. Create the ``current`` container: .. code:: - # curl -i $publicURL/current -X PUT -H "Content-Length: 0" -H "X-Auth-Token: $token" -H "X-Versions-Location: archive" -H "X-Versions-Mode: stack" + # curl -i $publicURL/current -X PUT -H "Content-Length: 0" -H "X-Auth-Token: $token" -H "X-Versions-Location: archive" .. code:: @@ -169,14 +176,14 @@ Example Using ``stack`` Mode on it. If want to completely remove an object and you have five versions of it, you must **DELETE** it five times. -Example Using ``history`` Mode ------------------------------- +Example Using ``X-History-Location`` +------------------------------------ #. Create the ``current`` container: .. code:: - # curl -i $publicURL/current -X PUT -H "Content-Length: 0" -H "X-Auth-Token: $token" -H "X-Versions-Location: archive" -H "X-Versions-Mode: history" + # curl -i $publicURL/current -X PUT -H "Content-Length: 0" -H "X-Auth-Token: $token" -H "X-History-Location: archive" .. code:: @@ -266,7 +273,7 @@ Example Using ``history`` Mode #. Issue a **DELETE** request to a versioned object to copy the current version of the object to the archive container then delete it from the current container. Subsequent **GET** requests to the object in the - current container will return 404 Not Found. + current container will return ``404 Not Found``. .. code:: diff --git a/swift/common/middleware/versioned_writes.py b/swift/common/middleware/versioned_writes.py index 3b340c1184..437a94b385 100644 --- a/swift/common/middleware/versioned_writes.py +++ b/swift/common/middleware/versioned_writes.py @@ -15,21 +15,48 @@ """ Object versioning in swift is implemented by setting a flag on the container -to tell swift to version all objects in the container. The flag is the -``X-Versions-Location`` header on the container, and its value is the -container where the versions are stored. +to tell swift to version all objects in the container. The value of the flag is +the container where the versions are stored (commonly referred to as the +"archive container"). The flag itself is one of two headers, which determines +how object ``DELETE`` requests are handled: + + * ``X-History-Location`` + + On ``DELETE``, copy the current version of the object to the archive + container, write a zero-byte "delete marker" object that notes when the + delete took place, and delete the object from the versioned container. The + object will no longer appear in container listings for the versioned + container and future requests there will return ``404 Not Found``. However, + the content will still be recoverable from the archive container. + + * ``X-Versions-Location`` + + On ``DELETE``, only remove the current version of the object. If any + previous versions exist in the archive container, the most recent one is + copied over the current version, and the copy in the archive container is + deleted. As a result, if you have 5 total versions of the object, you must + delete the object 5 times for that object name to start responding with + ``404 Not Found``. + +Either header may be used for the various containers within an account, but +only one may be set for any given container. Attempting to set both +simulataneously will result in a ``400 Bad Request`` response. .. note:: - It is recommended to use a different ``X-Versions-Location`` container for + It is recommended to use a different archive container for each container that is being versioned. +.. note:: + Enabling versioning on an archive container is not recommended. + When data is ``PUT`` into a versioned container (a container with the versioning flag turned on), the existing data in the file is redirected to a -new object and the data in the ``PUT`` request is saved as the data for the -versioned object. The new object name (for the previous version) is -``//``, where ``length`` -is the 3-character zero-padded hexadecimal length of the ```` and -```` is the timestamp of when the previous version was created. +new object in the archive container and the data in the ``PUT`` request is +saved as the data for the versioned object. The new object name (for the +previous version) is ``//``, +where ``length`` is the 3-character zero-padded hexadecimal length of the +```` and ```` is the timestamp of when the previous +version was created. A ``GET`` to a versioned object will return the current version of the object without having to do any request redirects or metadata lookups. @@ -39,38 +66,15 @@ but will not create a new version of the object. In other words, new versions are only created when the content of the object changes. A ``DELETE`` to a versioned object will be handled in one of two ways, -depending on the value of a ``X-Versions-Mode`` header set on the container. -The available modes are: - - * ``stack`` - - Only remove the current version of the object. If any previous versions - exist in the archive container, the most recent one is copied over the - current version, and the copy in the archive container is deleted. As a - result, if you have 5 total versions of the object, you must delete the - object 5 times to completely remove the object. This is the default - behavior if ``X-Versions-Mode`` has not been set for the container. - - * ``history`` - - Copy the current version of the object to the archive container, write - a zero-byte "delete marker" object that notes when the delete took place, - and delete the object from the versioned container. The object will no - longer appear in container listings for the versioned container and future - requests there will return 404 Not Found. However, the content will still - be recoverable from the archive container. - -.. note:: - While it is possible to switch between 'stack' and 'history' mode on a - container, it is not recommended. +as described above. To restore a previous version of an object, find the desired version in the archive container then issue a ``COPY`` with a ``Destination`` header -indicating the original location. This will retain a copy of the current -version similar to a ``PUT`` over the versioned object. Additionally, if the -container is in ``stack`` mode and the client wishes to permanently delete the -current version, it may issue a ``DELETE`` to the versioned object as -described above. +indicating the original location. This will archive the current version similar +to a ``PUT`` over the versioned object. If the the client additionally wishes +to permanently delete what was the current version, it must find the +newly-created archive in the archive container and issue a separate ``DELETE`` +to it. -------------------------------------------------- How to Enable Object Versioning in a Swift Cluster @@ -81,9 +85,10 @@ so this functionality was already available in previous releases and every attempt was made to maintain backwards compatibility. To allow operators to perform a seamless upgrade, it is not required to add the middleware to the proxy pipeline and the flag ``allow_versions`` in the container server -configuration files are still valid, but only for ``stack`` mode. In future -releases, ``allow_versions`` will be deprecated in favor of adding this -middleware to the pipeline to enable or disable the feature. +configuration files are still valid, but only when using +``X-Versions-Location``. In future releases, ``allow_versions`` will be +deprecated in favor of adding this middleware to the pipeline to enable or +disable the feature. In case the middleware is added to the proxy pipeline, you must also set ``allow_versioned_writes`` to ``True`` in the middleware options @@ -92,9 +97,9 @@ request. .. note:: You need to add the middleware to the proxy pipeline and set - ``allow_versioned_writes = True`` to use the ``history`` mode. Setting + ``allow_versioned_writes = True`` to use ``X-History-Location``. Setting ``allow_versions = True`` in the container server is not sufficient to - enable ``history`` mode. + enable the use of ``X-History-Location``. Upgrade considerations: @@ -103,25 +108,25 @@ Upgrade considerations: If ``allow_versioned_writes`` is set in the filter configuration, you can leave the ``allow_versions`` flag in the container server configuration files untouched. If you decide to disable or remove the ``allow_versions`` flag, you -must re-set any existing containers that had the 'X-Versions-Location' flag +must re-set any existing containers that had the ``X-Versions-Location`` flag configured so that it can now be tracked by the versioned_writes middleware. -Clients should not use the 'history' mode until all proxies in the cluster -have been upgraded to a version of Swift that supports it. Attempting to use -the 'history' mode during a rolling upgrade may result in some requests being -served by proxies running old code (which necessarily uses the 'stack' mode), -leading to data loss. +Clients should not use the ``X-History-Location`` header until all proxies in +the cluster have been upgraded to a version of Swift that supports it. +Attempting to use ``X-History-Location`` during a rolling upgrade may result +in some requests being served by proxies running old code, leading to data +loss. --------------------------------------------- -Examples Using ``curl`` with ``stack`` Mode --------------------------------------------- +---------------------------------------------------- +Examples Using ``curl`` with ``X-Versions-Location`` +---------------------------------------------------- First, create a container with the ``X-Versions-Location`` header or add the header to an existing container. Also make sure the container referenced by the ``X-Versions-Location`` exists. In this example, the name of that container is "versions":: - curl -i -XPUT -H "X-Auth-Token: " -H "X-Versions-Mode: stack" \ + curl -i -XPUT -H "X-Auth-Token: " \ -H "X-Versions-Location: versions" http:///container curl -i -XPUT -H "X-Auth-Token: " http:///versions @@ -150,16 +155,16 @@ http:///versions?prefix=008myobject/ curl -i -XGET -H "X-Auth-Token: " \ http:///container/myobject ----------------------------------------------- -Examples Using ``curl`` with ``history`` Mode ----------------------------------------------- +--------------------------------------------------- +Examples Using ``curl`` with ``X-History-Location`` +--------------------------------------------------- -As above, create a container with the ``X-Versions-Location`` header and ensure -that the container referenced by the ``X-Versions-Location`` exists. In this +As above, create a container with the ``X-History-Location`` header and ensure +that the container referenced by the ``X-History-Location`` exists. In this example, the name of that container is "versions":: - curl -i -XPUT -H "X-Auth-Token: " -H "X-Versions-Mode: history" \ --H "X-Versions-Location: versions" http:///container + curl -i -XPUT -H "X-Auth-Token: " \ +-H "X-History-Location: versions" http:///container curl -i -XPUT -H "X-Auth-Token: " http:///versions Create an object (the first version):: @@ -238,12 +243,11 @@ from swift.common.exceptions import ( ListingIterNotFound, ListingIterError) -VERSIONING_MODES = ('stack', 'history') DELETE_MARKER_CONTENT_TYPE = 'application/x-deleted;swift_versions_deleted=1' -VERSIONS_LOC_CLIENT = 'x-versions-location' -VERSIONS_LOC_SYSMETA = get_sys_meta_prefix('container') + 'versions-location' -VERSIONS_MODE_CLIENT = 'x-versions-mode' -VERSIONS_MODE_SYSMETA = get_sys_meta_prefix('container') + 'versions-mode' +CLIENT_VERSIONS_LOC = 'x-versions-location' +CLIENT_HISTORY_LOC = 'x-history-location' +SYSMETA_VERSIONS_LOC = get_sys_meta_prefix('container') + 'versions-location' +SYSMETA_VERSIONS_MODE = get_sys_meta_prefix('container') + 'versions-mode' class VersionedWritesContext(WSGIContext): @@ -646,17 +650,18 @@ class VersionedWritesContext(WSGIContext): self._response_headers = [] mode = location = '' for key, val in self._response_headers: - if key.lower() == VERSIONS_LOC_SYSMETA: + if key.lower() == SYSMETA_VERSIONS_LOC: location = val - elif key.lower() == VERSIONS_MODE_SYSMETA: + elif key.lower() == SYSMETA_VERSIONS_MODE: mode = val if location: - self._response_headers.extend([ - (VERSIONS_LOC_CLIENT.title(), location)]) - if mode: - self._response_headers.extend([ - (VERSIONS_MODE_CLIENT.title(), mode)]) + if mode == 'history': + self._response_headers.extend([ + (CLIENT_HISTORY_LOC.title(), location)]) + else: + self._response_headers.extend([ + (CLIENT_VERSIONS_LOC.title(), location)]) start_response(self._response_status, self._response_headers, @@ -672,61 +677,70 @@ class VersionedWritesMiddleware(object): self.logger = get_logger(conf, log_route='versioned_writes') def container_request(self, req, start_response, enabled): - # set version location header as sysmeta - if VERSIONS_LOC_CLIENT in req.headers: - val = req.headers.get(VERSIONS_LOC_CLIENT) - if val: + if CLIENT_VERSIONS_LOC in req.headers and \ + CLIENT_HISTORY_LOC in req.headers: + if not req.headers[CLIENT_HISTORY_LOC]: + # defer to versions location entirely + del req.headers[CLIENT_HISTORY_LOC] + elif req.headers[CLIENT_VERSIONS_LOC]: + raise HTTPBadRequest( + request=req, content_type='text/plain', + body='Only one of %s or %s may be specified' % ( + CLIENT_VERSIONS_LOC, CLIENT_HISTORY_LOC)) + else: + # history location is present and versions location is + # present but empty -- clean it up + del req.headers[CLIENT_VERSIONS_LOC] + + if CLIENT_VERSIONS_LOC in req.headers or \ + CLIENT_HISTORY_LOC in req.headers: + if CLIENT_VERSIONS_LOC in req.headers: + val = req.headers[CLIENT_VERSIONS_LOC] + mode = 'stack' + else: + val = req.headers[CLIENT_HISTORY_LOC] + mode = 'history' + + if not val: + # empty value is the same as X-Remove-Versions-Location + req.headers['X-Remove-Versions-Location'] = 'x' + elif not config_true_value(enabled) and \ + req.method in ('PUT', 'POST'): # differently from previous version, we are actually # returning an error if user tries to set versions location # while feature is explicitly disabled. - if not config_true_value(enabled) and \ - req.method in ('PUT', 'POST'): - raise HTTPPreconditionFailed( - request=req, content_type='text/plain', - body='Versioned Writes is disabled') - + raise HTTPPreconditionFailed( + request=req, content_type='text/plain', + body='Versioned Writes is disabled') + else: + # OK, we received a value, have versioning enabled, and aren't + # trying to set two modes at once. Validate the value and + # translate to sysmeta. location = check_container_format(req, val) - req.headers[VERSIONS_LOC_SYSMETA] = location + req.headers[SYSMETA_VERSIONS_LOC] = location + req.headers[SYSMETA_VERSIONS_MODE] = mode - # reset original header to maintain sanity + # reset original header on container server to maintain sanity # now only sysmeta is source of Versions Location - req.headers[VERSIONS_LOC_CLIENT] = '' + req.headers[CLIENT_VERSIONS_LOC] = '' - # if both headers are in the same request + # if both add and remove headers are in the same request # adding location takes precedence over removing - if 'X-Remove-Versions-Location' in req.headers: - del req.headers['X-Remove-Versions-Location'] - else: - # empty value is the same as X-Remove-Versions-Location - req.headers['X-Remove-Versions-Location'] = 'x' + for header in ['X-Remove-Versions-Location', + 'X-Remove-History-Location']: + if header in req.headers: + del req.headers[header] - # handle removing versions container - val = req.headers.get('X-Remove-Versions-Location') - if val: - req.headers.update({VERSIONS_LOC_SYSMETA: '', - VERSIONS_LOC_CLIENT: ''}) - del req.headers['X-Remove-Versions-Location'] - - # handle versioning mode - if VERSIONS_MODE_CLIENT in req.headers: - val = req.headers.pop(VERSIONS_MODE_CLIENT) - if val: - if not config_true_value(enabled) and \ - req.method in ('PUT', 'POST'): - raise HTTPPreconditionFailed( - request=req, content_type='text/plain', - body='Versioned Writes is disabled') - if val not in VERSIONING_MODES: - raise HTTPBadRequest( - request=req, content_type='text/plain', - body='X-Versions-Mode must be one of %s' % ', '.join( - VERSIONING_MODES)) - req.headers[VERSIONS_MODE_SYSMETA] = val - else: - req.headers['X-Remove-Versions-Mode'] = 'x' - - if req.headers.pop('X-Remove-Versions-Mode', None): - req.headers.update({VERSIONS_MODE_SYSMETA: ''}) + if any(req.headers.get(header) for header in [ + 'X-Remove-Versions-Location', + 'X-Remove-History-Location']): + req.headers.update({CLIENT_VERSIONS_LOC: '', + SYSMETA_VERSIONS_LOC: '', + SYSMETA_VERSIONS_MODE: ''}) + for header in ['X-Remove-Versions-Location', + 'X-Remove-History-Location']: + if header in req.headers: + del req.headers[header] # send request and translate sysmeta headers from response vw_ctx = VersionedWritesContext(self.app, self.logger) @@ -826,8 +840,8 @@ def filter_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) if config_true_value(conf.get('allow_versioned_writes')): - register_swift_info('versioned_writes', - allowed_versions_mode=VERSIONING_MODES) + register_swift_info('versioned_writes', allowed_flags=( + CLIENT_VERSIONS_LOC, CLIENT_HISTORY_LOC)) def obj_versions_filter(app): return VersionedWritesMiddleware(app, conf) diff --git a/test/unit/common/middleware/test_versioned_writes.py b/test/unit/common/middleware/test_versioned_writes.py index 1c3176911e..af1676da2b 100644 --- a/test/unit/common/middleware/test_versioned_writes.py +++ b/test/unit/common/middleware/test_versioned_writes.py @@ -123,15 +123,18 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): self.assertEqual('PUT', method) self.assertEqual('/v1/a/c', path) self.assertIn('x-container-sysmeta-versions-location', req_headers) - self.assertNotIn('x-container-sysmeta-versions-mode', req_headers) + self.assertEqual(req.headers['x-container-sysmeta-versions-location'], + 'ver_cont') + self.assertIn('x-container-sysmeta-versions-mode', req_headers) + self.assertEqual(req.headers['x-container-sysmeta-versions-mode'], + 'stack') self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) - def test_put_container_history(self): + def test_put_container_history_header(self): self.app.register('PUT', '/v1/a/c', swob.HTTPOk, {}, 'passed') req = Request.blank('/v1/a/c', - headers={'X-Versions-Location': 'ver_cont', - 'X-Versions-Mode': 'history'}, + headers={'X-History-Location': 'ver_cont'}, environ={'REQUEST_METHOD': 'PUT'}) status, headers, body = self.call_vw(req) self.assertEqual(status, '200 OK') @@ -150,17 +153,29 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) + def test_put_container_both_headers(self): + req = Request.blank('/v1/a/c', + headers={'X-Versions-Location': 'ver_cont', + 'X-History-Location': 'ver_cont'}, + environ={'REQUEST_METHOD': 'PUT'}) + status, headers, body = self.call_vw(req) + self.assertEqual(status, '400 Bad Request') + self.assertFalse(self.app.calls) + def test_container_allow_versioned_writes_false(self): self.vw.conf = {'allow_versioned_writes': 'false'} # PUT/POST container must fail as 412 when allow_versioned_writes # set to false for method in ('PUT', 'POST'): - req = Request.blank('/v1/a/c', - headers={'X-Versions-Location': 'ver_cont'}, - environ={'REQUEST_METHOD': method}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, "412 Precondition Failed") + for header in ('X-Versions-Location', 'X-History-Location'): + req = Request.blank('/v1/a/c', + headers={header: 'ver_cont'}, + environ={'REQUEST_METHOD': method}) + status, headers, body = self.call_vw(req) + self.assertEqual(status, "412 Precondition Failed", + 'Got %s instead of 412 when %sing ' + 'with %s header' % (status, method, header)) # GET performs as normal self.app.register('GET', '/v1/a/c', swob.HTTPOk, {}, 'passed') @@ -172,117 +187,34 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): status, headers, body = self.call_vw(req) self.assertEqual(status, '200 OK') - def test_remove_versions_location(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') + def _test_removal(self, headers): + self.app.register('POST', '/v1/a/c', swob.HTTPNoContent, {}, 'passed') req = Request.blank('/v1/a/c', - headers={'X-Remove-Versions-Location': 'x'}, + headers=headers, environ={'REQUEST_METHOD': 'POST'}) status, headers, body = self.call_vw(req) - self.assertEqual(status, '200 OK') + self.assertEqual(status, '204 No Content') # check for sysmeta header calls = self.app.calls_with_headers method, path, req_headers = calls[0] self.assertEqual('POST', method) self.assertEqual('/v1/a/c', path) - self.assertIn('x-container-sysmeta-versions-location', req_headers) - self.assertEqual('', - req_headers['x-container-sysmeta-versions-location']) - self.assertIn('x-versions-location', req_headers) - self.assertEqual('', req_headers['x-versions-location']) + for header in ['x-container-sysmeta-versions-location', + 'x-container-sysmeta-versions-mode', + 'x-versions-location']: + self.assertIn(header, req_headers) + self.assertEqual('', req_headers[header]) self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) + def test_remove_headers(self): + self._test_removal({'X-Remove-Versions-Location': 'x'}) + self._test_removal({'X-Remove-History-Location': 'x'}) + def test_empty_versions_location(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') - req = Request.blank('/v1/a/c', - headers={'X-Versions-Location': ''}, - environ={'REQUEST_METHOD': 'POST'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '200 OK') - - # check for sysmeta header - calls = self.app.calls_with_headers - method, path, req_headers = calls[0] - self.assertEqual('POST', method) - self.assertEqual('/v1/a/c', path) - self.assertIn('x-container-sysmeta-versions-location', req_headers) - self.assertEqual('', - req_headers['x-container-sysmeta-versions-location']) - self.assertIn('x-versions-location', req_headers) - self.assertEqual('', req_headers['x-versions-location']) - self.assertEqual(len(self.authorized), 1) - self.assertRequestEqual(req, self.authorized[0]) - - def test_post_versions_mode(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') - req = Request.blank('/v1/a/c', - headers={'X-Versions-Mode': 'stack'}, - environ={'REQUEST_METHOD': 'POST'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '200 OK') - - # check for sysmeta header - calls = self.app.calls_with_headers - method, path, req_headers = calls[0] - self.assertEqual('POST', method) - self.assertEqual('/v1/a/c', path) - self.assertIn('x-container-sysmeta-versions-mode', req_headers) - self.assertEqual('stack', - req_headers['x-container-sysmeta-versions-mode']) - self.assertNotIn('x-versions-mode', req_headers) - self.assertEqual(len(self.authorized), 1) - self.assertRequestEqual(req, self.authorized[0]) - - def test_remove_versions_mode(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') - req = Request.blank('/v1/a/c', - headers={'X-Remove-Versions-Mode': 'x'}, - environ={'REQUEST_METHOD': 'POST'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '200 OK') - - # check for sysmeta header - calls = self.app.calls_with_headers - method, path, req_headers = calls[0] - self.assertEqual('POST', method) - self.assertEqual('/v1/a/c', path) - self.assertIn('x-container-sysmeta-versions-mode', req_headers) - self.assertEqual('', - req_headers['x-container-sysmeta-versions-mode']) - self.assertNotIn('x-versions-mode', req_headers) - self.assertEqual(len(self.authorized), 1) - self.assertRequestEqual(req, self.authorized[0]) - - def test_empty_versions_mode(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') - req = Request.blank('/v1/a/c', - headers={'X-Versions-Mode': ''}, - environ={'REQUEST_METHOD': 'POST'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '200 OK') - - # check for sysmeta header - calls = self.app.calls_with_headers - method, path, req_headers = calls[0] - self.assertEqual('POST', method) - self.assertEqual('/v1/a/c', path) - self.assertIn('x-container-sysmeta-versions-mode', req_headers) - self.assertEqual('', - req_headers['x-container-sysmeta-versions-mode']) - self.assertNotIn('x-versions-mode', req_headers) - self.assertEqual(len(self.authorized), 1) - self.assertRequestEqual(req, self.authorized[0]) - - def test_bad_versions_mode(self): - self.app.register('POST', '/v1/a/c', swob.HTTPOk, {}, 'passed') - req = Request.blank('/v1/a/c', - headers={'X-Versions-Mode': 'foo'}, - environ={'REQUEST_METHOD': 'POST'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '400 Bad Request') - self.assertEqual(len(self.authorized), 0) - self.assertEqual('X-Versions-Mode must be one of stack, history', body) + self._test_removal({'X-Versions-Location': ''}) + self._test_removal({'X-History-Location': ''}) def test_remove_add_versions_precedence(self): self.app.register( @@ -308,6 +240,43 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) + def _test_blank_add_versions_precedence(self, blank_header, add_header): + self.app.register( + 'POST', '/v1/a/c', swob.HTTPOk, + {'x-container-sysmeta-versions-location': 'ver_cont'}, + 'passed') + req = Request.blank('/v1/a/c', + headers={blank_header: '', + add_header: 'ver_cont'}, + environ={'REQUEST_METHOD': 'POST'}) + + status, headers, body = self.call_vw(req) + self.assertEqual(status, '200 OK') + + # check for sysmeta header + calls = self.app.calls_with_headers + method, path, req_headers = calls[-1] + self.assertEqual('POST', method) + self.assertEqual('/v1/a/c', path) + self.assertIn('x-container-sysmeta-versions-location', req_headers) + self.assertEqual('ver_cont', + req_headers['x-container-sysmeta-versions-location']) + self.assertIn('x-container-sysmeta-versions-mode', req_headers) + self.assertEqual('history' if add_header == 'X-History-Location' + else 'stack', + req_headers['x-container-sysmeta-versions-mode']) + self.assertNotIn('x-remove-versions-location', req_headers) + self.assertIn('x-versions-location', req_headers) + self.assertEqual('', req_headers['x-versions-location']) + self.assertEqual(len(self.authorized), 1) + self.assertRequestEqual(req, self.authorized[0]) + + def test_blank_add_versions_precedence(self): + self._test_blank_add_versions_precedence( + 'X-Versions-Location', 'X-History-Location') + self._test_blank_add_versions_precedence( + 'X-History-Location', 'X-Versions-Location') + def test_get_container(self): self.app.register( 'GET', '/v1/a/c', swob.HTTPOk, @@ -319,7 +288,6 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): status, headers, body = self.call_vw(req) self.assertEqual(status, '200 OK') self.assertIn(('X-Versions-Location', 'ver_cont'), headers) - self.assertIn(('X-Versions-Mode', 'stack'), headers) self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) @@ -333,8 +301,7 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): environ={'REQUEST_METHOD': 'HEAD'}) status, headers, body = self.call_vw(req) self.assertEqual(status, '200 OK') - self.assertIn(('X-Versions-Location', 'other_ver_cont'), headers) - self.assertIn(('X-Versions-Mode', 'history'), headers) + self.assertIn(('X-History-Location', 'other_ver_cont'), headers) self.assertEqual(len(self.authorized), 1) self.assertRequestEqual(req, self.authorized[0]) @@ -850,17 +817,6 @@ class VersionedWritesTestCase(VersionedWritesBaseTestCase): self.assertTrue(path.startswith('/v1/a/ver_cont/001o/3')) self.assertNotIn('x-if-delete-at', [h.lower() for h in req_headers]) - def test_post_bad_mode(self): - req = Request.blank( - '/v1/a/c', - environ={'REQUEST_METHOD': 'POST', - 'CONTENT_LENGTH': '0', - 'HTTP_X_VERSIONS_MODE': 'bad-mode'}) - status, headers, body = self.call_vw(req) - self.assertEqual(status, '400 Bad Request') - self.assertEqual('X-Versions-Mode must be one of stack, history', body) - self.assertFalse(self.app.calls_with_headers) - @mock.patch('swift.common.middleware.versioned_writes.time.time', return_value=1234) def test_history_delete_marker_no_object_success(self, mock_time): @@ -1526,8 +1482,8 @@ class TestSwiftInfo(unittest.TestCase): swift_info = utils.get_swift_info() self.assertIn('versioned_writes', swift_info) self.assertEqual( - swift_info['versioned_writes'].get('allowed_versions_mode'), - ('stack', 'history')) + swift_info['versioned_writes'].get('allowed_flags'), + ('x-versions-location', 'x-history-location')) if __name__ == '__main__':