Use separate headers for versioned_writes' stack and history modes

Now, instead of saying

   X-Versions-Location: <container>
   X-Versions-Mode: history

clients should just say

   X-History-Location: <container>

Since we've never had a release featuring a user-settable
X-Versions-Mode header, support may be dropped and that is now ignored.

Change-Id: Icfd0f481d4e40dd5375c737190aea7ee8dbc3bf9
This commit is contained in:
Tim Burke 2016-09-20 13:54:55 -07:00
parent b4c1517ddd
commit 60a2fe0ba8
5 changed files with 281 additions and 293 deletions

View File

@ -621,6 +621,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
<http://docs.openstack.org/ developer/swift/api/object_versioning.html>`_.
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 <http://docs.openstack.org/developer/
swift/api/object_versioning.html>`_.
in: header
required: false
type: string
X-Newest:
description: |
If set to true , Object Storage queries all
@ -683,9 +704,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
@ -756,10 +785,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 <http://docs.openstack.org/developer/
swift/api/object_versioning.html>`_.
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
<http://docs.openstack.org/ developer/swift/api/object_versioning.html>`_.
in: header
required: false
type: string
@ -773,26 +803,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 <http://docs.openstack.org/developer/
swift/api/object_versioning.html>`_.
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 <http://docs.openstack.org/developer/
swift/api/object_versioning.html>`_.
in: header
required: false
type: string
# variables in path
account:

View File

@ -83,7 +83,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
@ -197,7 +197,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
@ -331,8 +331,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
@ -436,7 +437,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

View File

@ -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::

View File

@ -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
``<archive_container>/<length><object_name>/<timestamp>``, where ``length``
is the 3-character zero-padded hexadecimal length of the ``<object_name>`` and
``<timestamp>`` 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 ``<archive_container>/<length><object_name>/<timestamp>``,
where ``length`` is the 3-character zero-padded hexadecimal length of the
``<object_name>`` and ``<timestamp>`` 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: <token>" -H "X-Versions-Mode: stack" \
curl -i -XPUT -H "X-Auth-Token: <token>" \
-H "X-Versions-Location: versions" http://<storage_url>/container
curl -i -XPUT -H "X-Auth-Token: <token>" http://<storage_url>/versions
@ -150,16 +155,16 @@ http://<storage_url>/versions?prefix=008myobject/
curl -i -XGET -H "X-Auth-Token: <token>" \
http://<storage_url>/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: <token>" -H "X-Versions-Mode: history" \
-H "X-Versions-Location: versions" http://<storage_url>/container
curl -i -XPUT -H "X-Auth-Token: <token>" \
-H "X-History-Location: versions" http://<storage_url>/container
curl -i -XPUT -H "X-Auth-Token: <token>" http://<storage_url>/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)

View File

@ -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__':