Merge remote-tracking branch 'gerrit/master' into feature/deep
Change-Id: I812a5eb3ba7e9dc8fb8814f2785d33c2ecd090cb
This commit is contained in:
commit
7afa04edac
|
@ -16,6 +16,7 @@ import sys
|
|||
from optparse import OptionParser
|
||||
|
||||
from swift.cli.info import print_info, InfoSystemExit
|
||||
from swift.common.exceptions import LockTimeout
|
||||
|
||||
|
||||
def run_print_info(args, opts):
|
||||
|
@ -23,7 +24,7 @@ def run_print_info(args, opts):
|
|||
print_info('account', *args, **opts)
|
||||
except InfoSystemExit:
|
||||
sys.exit(1)
|
||||
except sqlite3.OperationalError as e:
|
||||
except (sqlite3.OperationalError, LockTimeout) as e:
|
||||
if not opts.get('stale_reads_ok'):
|
||||
opts['stale_reads_ok'] = True
|
||||
print('Warning: Possibly Stale Data')
|
||||
|
|
|
@ -16,6 +16,7 @@ import sys
|
|||
from optparse import OptionParser
|
||||
|
||||
from swift.cli.info import print_info, InfoSystemExit
|
||||
from swift.common.exceptions import LockTimeout
|
||||
|
||||
|
||||
def run_print_info(args, opts):
|
||||
|
@ -23,7 +24,7 @@ def run_print_info(args, opts):
|
|||
print_info('container', *args, **opts)
|
||||
except InfoSystemExit:
|
||||
sys.exit(1)
|
||||
except sqlite3.OperationalError as e:
|
||||
except (sqlite3.OperationalError, LockTimeout) as e:
|
||||
if not opts.get('stale_reads_ok'):
|
||||
opts['stale_reads_ok'] = True
|
||||
print('Warning: Possibly Stale Data')
|
||||
|
|
|
@ -29,10 +29,12 @@ if __name__ == '__main__':
|
|||
parser.add_argument('--devices', default='/srv/node',
|
||||
dest='devices', help='Path to swift device directory')
|
||||
parser.add_argument('--skip-mount-check', default=False,
|
||||
help='Don\'t test if disk is mounted',
|
||||
action="store_true", dest='skip_mount_check')
|
||||
parser.add_argument('--logfile', default=None,
|
||||
dest='logfile')
|
||||
parser.add_argument('--debug', default=False, action='store_true')
|
||||
dest='logfile', help='Set log file name')
|
||||
parser.add_argument('--debug', default=False, action='store_true',
|
||||
help='Enable debug mode')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
.\"
|
||||
.\" Copyright (c) 2017 OpenStack Foundation.
|
||||
.\"
|
||||
.\" Licensed under the Apache License, Version 2.0 (the "License");
|
||||
.\" you may not use this file except in compliance with the License.
|
||||
.\" You may obtain a copy of the License at
|
||||
.\"
|
||||
.\" http://www.apache.org/licenses/LICENSE-2.0
|
||||
.\"
|
||||
.\" Unless required by applicable law or agreed to in writing, software
|
||||
.\" distributed under the License is distributed on an "AS IS" BASIS,
|
||||
.\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
.\" implied.
|
||||
.\" See the License for the specific language governing permissions and
|
||||
.\" limitations under the License.
|
||||
.\"
|
||||
.TH SWIFT-OBJECT-RELINKER "1" "December 2017" "OpenStack Swift"
|
||||
|
||||
.SH NAME
|
||||
\fBswift\-object\-relinker\fR \- relink and cleanup objects to increase partition power
|
||||
.SH SYNOPSIS
|
||||
.B swift\-object\-relinker
|
||||
[\fIoptions\fR] <\fIcommand\fR>
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The relinker prepares an object server’s filesystem for a partition power
|
||||
change by crawling the filesystem and linking existing objects to future
|
||||
partition directories.
|
||||
|
||||
More information can be found at
|
||||
.BI https://docs.openstack.org/swift/latest/ring_partpower.html
|
||||
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
\fBrelink\fR
|
||||
Relink files for partition power increase.
|
||||
|
||||
.TP
|
||||
\fBcleanup\fR
|
||||
Remove hard links in the old locations.
|
||||
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
Show this help message and exit
|
||||
|
||||
.TP
|
||||
\fB\-\-swift-dir\fR \fISWIFT_DIR\fR
|
||||
Path to swift directory
|
||||
|
||||
.TP
|
||||
\fB\-\-devices\fR \fIDEVICES\fR
|
||||
Path to swift device directory
|
||||
|
||||
.TP
|
||||
\fB\-\-skip\-mount\-check\fR
|
||||
Don't test if disk is mounted
|
||||
|
||||
.TP
|
||||
\fB\-\-logfile\fR \fILOGFILE\fR
|
||||
Set log file name
|
||||
|
||||
.TP
|
||||
\fB\-\-debug\fR
|
||||
Enable debug mode
|
||||
|
||||
.SH DOCUMENTATION
|
||||
.LP
|
||||
More in depth documentation in regards to
|
||||
.BI swift\-object\-relinker
|
||||
and also about OpenStack Swift as a whole can be found at
|
||||
.BI http://docs.openstack.org/developer/swift/index.html
|
||||
and
|
||||
.BI http://docs.openstack.org
|
|
@ -0,0 +1,8 @@
|
|||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
# this is required for the docs build jobs
|
||||
sphinx>=1.6.2 # BSD
|
||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||
reno>=1.8.0 # Apache-2.0
|
||||
os-api-ref>=1.0.0 # Apache-2.0
|
|
@ -249,6 +249,98 @@ However, the request from the user **must** contain the appropriate
|
|||
`Referer` header, the referrer ACL has very weak security.
|
||||
|
||||
|
||||
Example: Sharing a Container with Another User
|
||||
----------------------------------------------
|
||||
|
||||
Sharing a Container with another user requires the knowledge of few
|
||||
parameters regarding the users.
|
||||
|
||||
The sharing user must know:
|
||||
|
||||
- the ``OpenStack user id`` of the other user
|
||||
|
||||
The sharing user must communicate to the other user:
|
||||
|
||||
- the name of the shared container
|
||||
- the ``OS_STORAGE_URL``
|
||||
|
||||
Usually the ``OS_STORAGE_URL`` is not exposed directly to the user
|
||||
because the ``swift client`` by default automatically construct the
|
||||
``OS_STORAGE_URL`` based on the User credential.
|
||||
|
||||
We assume that in the current directory there are the two client
|
||||
environment script for the two users ``sharing.openrc`` and
|
||||
``other.openrc``.
|
||||
|
||||
The ``sharing.openrc`` should be similar to the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export OS_USERNAME=sharing
|
||||
# WARNING: Save the password in clear text only for testing purposes
|
||||
export OS_PASSWORD=password
|
||||
export OS_TENANT_NAME=projectName
|
||||
export OS_AUTH_URL=https://identityHost:portNumber/v2.0
|
||||
# The following lines can be omitted
|
||||
export OS_TENANT_ID=tenantIDString
|
||||
export OS_REGION_NAME=regionName
|
||||
export OS_CACERT=/path/to/cacertFile
|
||||
|
||||
The ``other.openrc`` should be similar to the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export OS_USERNAME=other
|
||||
# WARNING: Save the password in clear text only for testing purposes
|
||||
export OS_PASSWORD=otherPassword
|
||||
export OS_TENANT_NAME=otherProjectName
|
||||
export OS_AUTH_URL=https://identityHost:portNumber/v2.0
|
||||
# The following lines can be omitted
|
||||
export OS_TENANT_ID=tenantIDString
|
||||
export OS_REGION_NAME=regionName
|
||||
export OS_CACERT=/path/to/cacertFile
|
||||
|
||||
For more information see `using the OpenStack RC file
|
||||
<https://docs.openstack.org/user-guide/common/cli-set-environment-variables-using-openstack-rc.html>`_
|
||||
|
||||
First we figure out the other user id::
|
||||
|
||||
. other.openrc
|
||||
OUID="$(openstack user show --format json "${OS_USERNAME}" | jq -r .id)"
|
||||
|
||||
or alternatively::
|
||||
|
||||
. other.openrc
|
||||
OUID="$(openstack token issue -f json | jq -r .user_id)"
|
||||
|
||||
Then we figure out the storage url of the sharing user::
|
||||
|
||||
sharing.openrc
|
||||
SURL="$(swift auth | awk -F = '/OS_STORAGE_URL/ {print $2}')"
|
||||
|
||||
Running as the sharing user create a shared container named ``shared``
|
||||
in read-only mode with the other user using the proper acl::
|
||||
|
||||
sharing.openrc
|
||||
swift post --read-acl "*:${OUID}" shared
|
||||
|
||||
Running as the sharing user create and upload a test file::
|
||||
|
||||
touch void
|
||||
swift upload shared void
|
||||
|
||||
Running as the other user list the files in the ``shared`` container::
|
||||
|
||||
other.openrc
|
||||
swift --os-storage-url="${SURL}" list shared
|
||||
|
||||
Running as the other user download the ``shared`` container in the
|
||||
``/tmp`` directory::
|
||||
|
||||
cd /tmp
|
||||
swift --os-storage-url="${SURL}" download shared
|
||||
|
||||
|
||||
.. _account_acls:
|
||||
|
||||
------------
|
||||
|
|
|
@ -115,11 +115,6 @@ paste.filter_factory =
|
|||
listing_formats = swift.common.middleware.listing_formats:filter_factory
|
||||
symlink = swift.common.middleware.symlink:filter_factory
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = doc/build
|
||||
source-dir = doc/source
|
||||
warning-is-error = 1
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
|
|
|
@ -332,7 +332,7 @@ from swift.common.utils import get_logger, config_true_value, \
|
|||
register_swift_info, RateLimitedIterator, quote, close_if_possible, \
|
||||
closing_if_possible, LRUCache, StreamingPile, strict_b64decode
|
||||
from swift.common.request_helpers import SegmentedIterable, \
|
||||
get_sys_meta_prefix, update_etag_is_at_header
|
||||
get_sys_meta_prefix, update_etag_is_at_header, resolve_etag_is_at_header
|
||||
from swift.common.constraints import check_utf8, MAX_BUFFERED_SLO_SEGMENTS
|
||||
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, is_success
|
||||
from swift.common.wsgi import WSGIContext, make_subrequest
|
||||
|
@ -792,16 +792,19 @@ class SloGetContext(WSGIContext):
|
|||
if slo_etag and slo_size and (
|
||||
req.method == 'HEAD' or is_conditional):
|
||||
# Since we have length and etag, we can respond immediately
|
||||
for i, (header, _value) in enumerate(self._response_headers):
|
||||
lheader = header.lower()
|
||||
if lheader == 'etag':
|
||||
self._response_headers[i] = (header, '"%s"' % slo_etag)
|
||||
elif lheader == 'content-length' and not is_conditional:
|
||||
self._response_headers[i] = (header, slo_size)
|
||||
start_response(self._response_status,
|
||||
self._response_headers,
|
||||
self._response_exc_info)
|
||||
return resp_iter
|
||||
resp = Response(
|
||||
status=self._response_status,
|
||||
headers=self._response_headers,
|
||||
app_iter=resp_iter,
|
||||
request=req,
|
||||
conditional_etag=resolve_etag_is_at_header(
|
||||
req, self._response_headers),
|
||||
conditional_response=True)
|
||||
resp.headers.update({
|
||||
'Etag': '"%s"' % slo_etag,
|
||||
'Content-Length': slo_size,
|
||||
})
|
||||
return resp(req.environ, start_response)
|
||||
|
||||
if self._need_to_refetch_manifest(req):
|
||||
req.environ['swift.non_client_disconnect'] = True
|
||||
|
@ -874,14 +877,15 @@ class SloGetContext(WSGIContext):
|
|||
response_headers = []
|
||||
for header, value in resp_headers:
|
||||
lheader = header.lower()
|
||||
if lheader not in ('etag', 'content-length'):
|
||||
response_headers.append((header, value))
|
||||
|
||||
if lheader == SYSMETA_SLO_ETAG:
|
||||
slo_etag = value
|
||||
elif lheader == SYSMETA_SLO_SIZE:
|
||||
# it's from sysmeta, so we don't worry about non-integer
|
||||
# values here
|
||||
content_length = int(value)
|
||||
elif lheader not in ('etag', 'content-length'):
|
||||
response_headers.append((header, value))
|
||||
|
||||
# Prep to calculate content_length & etag if necessary
|
||||
if slo_etag is None:
|
||||
|
@ -926,7 +930,9 @@ class SloGetContext(WSGIContext):
|
|||
req, content_length, response_headers, segments)
|
||||
|
||||
def _manifest_head_response(self, req, response_headers):
|
||||
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
||||
return HTTPOk(request=req, headers=response_headers, body='',
|
||||
conditional_etag=conditional_etag,
|
||||
conditional_response=True)
|
||||
|
||||
def _manifest_get_response(self, req, content_length, response_headers,
|
||||
|
@ -984,9 +990,11 @@ class SloGetContext(WSGIContext):
|
|||
# the proxy logs and the user will receive incomplete results.
|
||||
return HTTPConflict(request=req)
|
||||
|
||||
conditional_etag = resolve_etag_is_at_header(req, response_headers)
|
||||
response = Response(request=req, content_length=content_length,
|
||||
headers=response_headers,
|
||||
conditional_response=True,
|
||||
conditional_etag=conditional_etag,
|
||||
app_iter=segmented_iter)
|
||||
return response
|
||||
|
||||
|
|
|
@ -432,6 +432,8 @@ class TempAuth(object):
|
|||
expires, groups = cached_auth_data
|
||||
if expires < time():
|
||||
groups = None
|
||||
else:
|
||||
groups = groups.encode('utf8')
|
||||
|
||||
s3_auth_details = env.get('swift3.auth_details')
|
||||
if s3_auth_details:
|
||||
|
@ -788,7 +790,8 @@ class TempAuth(object):
|
|||
cached_auth_data = memcache_client.get(memcache_token_key)
|
||||
if cached_auth_data:
|
||||
expires, old_groups = cached_auth_data
|
||||
old_groups = old_groups.split(',')
|
||||
old_groups = [group.encode('utf8')
|
||||
for group in old_groups.split(',')]
|
||||
new_groups = self._get_user_groups(account, account_user,
|
||||
account_id)
|
||||
|
||||
|
|
|
@ -622,7 +622,10 @@ class Match(object):
|
|||
"""
|
||||
def __init__(self, headerval):
|
||||
self.tags = set()
|
||||
for tag in headerval.split(', '):
|
||||
for tag in headerval.split(','):
|
||||
tag = tag.strip()
|
||||
if not tag:
|
||||
continue
|
||||
if tag.startswith('"') and tag.endswith('"'):
|
||||
self.tags.add(tag[1:-1])
|
||||
else:
|
||||
|
@ -631,6 +634,10 @@ class Match(object):
|
|||
def __contains__(self, val):
|
||||
return '*' in self.tags or val in self.tags
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (
|
||||
self.__class__.__name__, ', '.join(sorted(self.tags)))
|
||||
|
||||
|
||||
class Accept(object):
|
||||
"""
|
||||
|
|
|
@ -4902,37 +4902,21 @@ def modify_priority(conf, logger):
|
|||
|
||||
|
||||
def o_tmpfile_in_path_supported(dirpath):
|
||||
if not hasattr(os, 'O_TMPFILE'):
|
||||
return False
|
||||
|
||||
testfile = os.path.join(dirpath, ".o_tmpfile.test")
|
||||
|
||||
hasO_TMPFILE = True
|
||||
fd = None
|
||||
try:
|
||||
fd = os.open(testfile, os.O_CREAT | os.O_WRONLY | os.O_TMPFILE)
|
||||
fd = os.open(dirpath, os.O_WRONLY | O_TMPFILE)
|
||||
return True
|
||||
except OSError as e:
|
||||
if e.errno == errno.EINVAL:
|
||||
hasO_TMPFILE = False
|
||||
if e.errno in (errno.EINVAL, errno.EISDIR, errno.EOPNOTSUPP):
|
||||
return False
|
||||
else:
|
||||
raise Exception("Error on '%(path)s' while checking "
|
||||
"O_TMPFILE: '%(ex)s'",
|
||||
{'path': dirpath, 'ex': e})
|
||||
|
||||
except Exception as e:
|
||||
raise Exception("Error on '%(path)s' while checking O_TMPFILE: "
|
||||
"'%(ex)s'", {'path': dirpath, 'ex': e})
|
||||
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
|
||||
# ensure closing the fd will actually remove the file
|
||||
if os.path.isfile(testfile):
|
||||
return False
|
||||
|
||||
return hasO_TMPFILE
|
||||
|
||||
|
||||
def o_tmpfile_in_tmpdir_supported():
|
||||
return o_tmpfile_in_path_supported(gettempdir())
|
||||
|
|
|
@ -8,14 +8,10 @@ coverage>=3.6 # Apache-2.0
|
|||
nose # LGPL
|
||||
nosexcover # BSD
|
||||
nosehtmloutput>=0.0.3 # Apache-2.0
|
||||
sphinx>=1.6.2 # BSD
|
||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||
os-api-ref>=1.0.0 # Apache-2.0
|
||||
os-testr>=0.8.0 # Apache-2.0
|
||||
mock>=2.0 # BSD
|
||||
python-swiftclient
|
||||
python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
reno>=1.8.0 # Apache-2.0
|
||||
|
||||
# Security checks
|
||||
bandit>=1.1.0 # Apache-2.0
|
||||
|
|
|
@ -20,7 +20,7 @@ from hashlib import md5
|
|||
from swift.common import swob
|
||||
from swift.common.header_key_dict import HeaderKeyDict
|
||||
from swift.common.request_helpers import is_user_meta, \
|
||||
is_object_transient_sysmeta
|
||||
is_object_transient_sysmeta, resolve_etag_is_at_header
|
||||
from swift.common.swob import HTTPNotImplemented
|
||||
from swift.common.utils import split_path
|
||||
|
||||
|
@ -154,11 +154,8 @@ class FakeSwift(object):
|
|||
self._calls.append(
|
||||
FakeSwiftCall(method, path, HeaderKeyDict(req.headers)))
|
||||
|
||||
backend_etag_header = req.headers.get('X-Backend-Etag-Is-At')
|
||||
conditional_etag = None
|
||||
if backend_etag_header and backend_etag_header in headers:
|
||||
# Apply conditional etag overrides
|
||||
conditional_etag = headers[backend_etag_header]
|
||||
# Apply conditional etag overrides
|
||||
conditional_etag = resolve_etag_is_at_header(req, headers)
|
||||
|
||||
# range requests ought to work, hence conditional_response=True
|
||||
if isinstance(body, list):
|
||||
|
|
|
@ -1392,6 +1392,7 @@ class TestSloHeadOldManifest(SloTestCase):
|
|||
'Content-Length': str(len(manifest_json)),
|
||||
'Content-Type': 'test/data',
|
||||
'X-Static-Large-Object': 'true',
|
||||
'X-Object-Sysmeta-Artisanal-Etag': 'bespoke',
|
||||
'Etag': md5hex(manifest_json)}
|
||||
manifest_headers.update(getattr(self, 'extra_manifest_headers', {}))
|
||||
self.manifest_has_sysmeta = all(h in manifest_headers for h in (
|
||||
|
@ -1449,6 +1450,46 @@ class TestSloHeadOldManifest(SloTestCase):
|
|||
expected_app_calls.append(('GET', '/v1/AUTH_test/headtest/man'))
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
|
||||
def test_if_none_match_etag_matching_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/headtest/man',
|
||||
environ={'REQUEST_METHOD': 'HEAD'},
|
||||
headers={
|
||||
'If-None-Match': 'bespoke',
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Artisanal-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
self.assertEqual(status, '304 Not Modified')
|
||||
# We *are not* responsible for replacing the etag; whoever set
|
||||
# x-backend-etag-is-at is responsible
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('Content-Length', '0'), headers)
|
||||
self.assertIn(('Content-Type', 'test/data'), headers)
|
||||
|
||||
expected_app_calls = [('HEAD', '/v1/AUTH_test/headtest/man')]
|
||||
if not self.manifest_has_sysmeta:
|
||||
expected_app_calls.append(('GET', '/v1/AUTH_test/headtest/man'))
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
|
||||
def test_if_match_etag_not_matching_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/headtest/man',
|
||||
environ={'REQUEST_METHOD': 'HEAD'},
|
||||
headers={
|
||||
'If-Match': self.slo_etag,
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Artisanal-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
self.assertEqual(status, '412 Precondition Failed')
|
||||
# We *are not* responsible for replacing the etag; whoever set
|
||||
# x-backend-etag-is-at is responsible
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('Content-Length', '0'), headers)
|
||||
self.assertIn(('Content-Type', 'test/data'), headers)
|
||||
|
||||
expected_app_calls = [('HEAD', '/v1/AUTH_test/headtest/man')]
|
||||
if not self.manifest_has_sysmeta:
|
||||
expected_app_calls.append(('GET', '/v1/AUTH_test/headtest/man'))
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
|
||||
|
||||
class TestSloHeadManifest(TestSloHeadOldManifest):
|
||||
def setUp(self):
|
||||
|
@ -3410,7 +3451,8 @@ class TestSloConditionalGetOldManifest(SloTestCase):
|
|||
'Content-Length': str(len(_abcd_manifest_json)),
|
||||
'Content-Type': 'application/json',
|
||||
'X-Static-Large-Object': 'true',
|
||||
'Etag': md5hex(_abcd_manifest_json)}
|
||||
'Etag': md5hex(_abcd_manifest_json),
|
||||
'X-Object-Sysmeta-Custom-Etag': 'a custom etag'}
|
||||
manifest_headers.update(getattr(self, 'extra_manifest_headers', {}))
|
||||
self.manifest_has_sysmeta = all(h in manifest_headers for h in (
|
||||
'X-Object-Sysmeta-Slo-Etag', 'X-Object-Sysmeta-Slo-Size'))
|
||||
|
@ -3521,6 +3563,128 @@ class TestSloConditionalGetOldManifest(SloTestCase):
|
|||
self.assertEqual(self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_none_match_matches_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'If-None-Match': '"a custom etag"',
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Custom-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
|
||||
self.assertEqual(status, '304 Not Modified')
|
||||
self.assertIn(('Content-Length', '0'), headers)
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('X-Object-Sysmeta-Custom-Etag', 'a custom etag'),
|
||||
headers)
|
||||
self.assertEqual(body, '')
|
||||
|
||||
expected_app_calls = [('GET', '/v1/AUTH_test/gettest/manifest-abcd')]
|
||||
if not self.manifest_has_sysmeta:
|
||||
# NB: no known middleware would have written a custom etag with
|
||||
# old-style manifests. but if there *was*, here's what'd happen
|
||||
expected_app_calls.extend([
|
||||
# 304, so gotta refetch
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
# Since the "authoritative" etag didn't come from slo, we still
|
||||
# verify the first segment
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
])
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
self.assertEqual(
|
||||
self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'X-Object-Sysmeta-Custom-Etag,x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_none_match_does_not_match_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'If-None-Match': "%s" % self.slo_etag,
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Custom-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
|
||||
self.assertEqual(status, '200 OK')
|
||||
self.assertIn(('Content-Length', '50'), headers)
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('X-Object-Sysmeta-Custom-Etag', 'a custom etag'),
|
||||
headers)
|
||||
self.assertEqual(
|
||||
body, 'aaaaabbbbbbbbbbcccccccccccccccdddddddddddddddddddd')
|
||||
|
||||
expected_app_calls = [
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/d_20?multipart-manifest=get'),
|
||||
]
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
self.assertEqual(
|
||||
self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'X-Object-Sysmeta-Custom-Etag,x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_matches_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'If-Match': '"a custom etag"',
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Custom-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
|
||||
self.assertEqual(status, '200 OK')
|
||||
self.assertIn(('Content-Length', '50'), headers)
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('X-Object-Sysmeta-Custom-Etag', 'a custom etag'),
|
||||
headers)
|
||||
self.assertEqual(
|
||||
body, 'aaaaabbbbbbbbbbcccccccccccccccdddddddddddddddddddd')
|
||||
|
||||
expected_app_calls = [
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
# Match on the override from left of us; no need to refetch
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/d_20?multipart-manifest=get'),
|
||||
]
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
self.assertEqual(
|
||||
self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'X-Object-Sysmeta-Custom-Etag,x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_does_not_match_with_override(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'If-Match': "%s" % self.slo_etag,
|
||||
'X-Backend-Etag-Is-At': 'X-Object-Sysmeta-Custom-Etag'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
|
||||
self.assertEqual(status, '412 Precondition Failed')
|
||||
self.assertIn(('Content-Length', '0'), headers)
|
||||
self.assertIn(('Etag', '"%s"' % self.slo_etag), headers)
|
||||
self.assertIn(('X-Object-Sysmeta-Custom-Etag', 'a custom etag'),
|
||||
headers)
|
||||
self.assertEqual(body, '')
|
||||
|
||||
expected_app_calls = [('GET', '/v1/AUTH_test/gettest/manifest-abcd')]
|
||||
if not self.manifest_has_sysmeta:
|
||||
# NB: no known middleware would have written a custom etag with
|
||||
# old-style manifests. but if there *was*, here's what'd happen
|
||||
expected_app_calls.extend([
|
||||
# Manifest never matches -> got back a 412; need to re-fetch
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
# We *still* verify the first segment, even though we'll 412
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
])
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
self.assertEqual(
|
||||
self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'X-Object-Sysmeta-Custom-Etag,x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_matches_and_range(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
|
|
|
@ -37,6 +37,14 @@ class FakeMemcache(object):
|
|||
return self.store.get(key)
|
||||
|
||||
def set(self, key, value, time=0):
|
||||
if isinstance(value, (tuple, list)):
|
||||
decoded = []
|
||||
for elem in value:
|
||||
if type(elem) == str:
|
||||
decoded.append(elem.decode('utf8'))
|
||||
else:
|
||||
decoded.append(elem)
|
||||
value = tuple(decoded)
|
||||
self.store[key] = value
|
||||
return True
|
||||
|
||||
|
@ -908,6 +916,37 @@ class TestAuth(unittest.TestCase):
|
|||
self.assertEqual(resp.headers.get('Www-Authenticate'),
|
||||
'Swift realm="BLAH_account"')
|
||||
|
||||
def test_successful_token_unicode_user(self):
|
||||
app = FakeApp(iter(NO_CONTENT_RESP))
|
||||
ath = auth.filter_factory(
|
||||
{u'user_t\u00e9st_t\u00e9ster'.encode('utf8'):
|
||||
u'p\u00e1ss .admin'.encode('utf8')})(app)
|
||||
memcache = FakeMemcache()
|
||||
|
||||
req = self._make_request(
|
||||
'/auth/v1.0',
|
||||
headers={'X-Auth-User': u't\u00e9st:t\u00e9ster',
|
||||
'X-Auth-Key': u'p\u00e1ss'})
|
||||
req.environ['swift.cache'] = memcache
|
||||
resp = req.get_response(ath)
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
auth_token = resp.headers['X-Auth-Token']
|
||||
|
||||
req = self._make_request(
|
||||
'/auth/v1.0',
|
||||
headers={'X-Auth-User': u't\u00e9st:t\u00e9ster',
|
||||
'X-Auth-Key': u'p\u00e1ss'})
|
||||
req.environ['swift.cache'] = memcache
|
||||
resp = req.get_response(ath)
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(auth_token, resp.headers['X-Auth-Token'])
|
||||
|
||||
req = self._make_request(
|
||||
u'/v1/AUTH_t\u00e9st', headers={'X-Auth-Token': auth_token})
|
||||
req.environ['swift.cache'] = memcache
|
||||
resp = req.get_response(ath)
|
||||
self.assertEqual(204, resp.status_int)
|
||||
|
||||
|
||||
class TestAuthWithMultiplePrefixes(TestAuth):
|
||||
"""
|
||||
|
|
|
@ -271,12 +271,14 @@ class TestMatch(unittest.TestCase):
|
|||
self.assertIn('a', match)
|
||||
self.assertIn('b', match)
|
||||
self.assertNotIn('c', match)
|
||||
self.assertEqual(repr(match), "Match('a, b')")
|
||||
|
||||
def test_match_star(self):
|
||||
match = swift.common.swob.Match('"a", "*"')
|
||||
self.assertIn('a', match)
|
||||
self.assertIn('b', match)
|
||||
self.assertIn('c', match)
|
||||
self.assertEqual(repr(match), "Match('*, a')")
|
||||
|
||||
def test_match_noquote(self):
|
||||
match = swift.common.swob.Match('a, b')
|
||||
|
@ -285,6 +287,20 @@ class TestMatch(unittest.TestCase):
|
|||
self.assertIn('b', match)
|
||||
self.assertNotIn('c', match)
|
||||
|
||||
def test_match_no_optional_white_space(self):
|
||||
match = swift.common.swob.Match('"a","b"')
|
||||
self.assertEqual(match.tags, set(('a', 'b')))
|
||||
self.assertIn('a', match)
|
||||
self.assertIn('b', match)
|
||||
self.assertNotIn('c', match)
|
||||
|
||||
def test_match_lots_of_optional_white_space(self):
|
||||
match = swift.common.swob.Match('"a" , , "b" ')
|
||||
self.assertEqual(match.tags, set(('a', 'b')))
|
||||
self.assertIn('a', match)
|
||||
self.assertIn('b', match)
|
||||
self.assertNotIn('c', match)
|
||||
|
||||
|
||||
class TestTransferEncoding(unittest.TestCase):
|
||||
def test_is_chunked(self):
|
||||
|
|
17
tox.ini
17
tox.ini
|
@ -26,9 +26,9 @@ setenv = VIRTUAL_ENV={envdir}
|
|||
NOSE_COVER_HTML=1
|
||||
NOSE_COVER_HTML_DIR={toxinidir}/cover
|
||||
|
||||
[testenv:py34]
|
||||
[testenv:py35]
|
||||
commands =
|
||||
nosetests \
|
||||
nosetests {posargs:\
|
||||
test/unit/cli/test_dispersion_report.py \
|
||||
test/unit/cli/test_form_signature.py \
|
||||
test/unit/cli/test_info.py \
|
||||
|
@ -45,10 +45,7 @@ commands =
|
|||
test/unit/common/test_splice.py \
|
||||
test/unit/common/test_storage_policy.py \
|
||||
test/unit/common/test_utils.py \
|
||||
test/unit/common/test_wsgi.py
|
||||
|
||||
[testenv:py35]
|
||||
commands = {[testenv:py34]commands}
|
||||
test/unit/common/test_wsgi.py}
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python2.7
|
||||
|
@ -92,15 +89,14 @@ commands = {posargs}
|
|||
|
||||
[testenv:docs]
|
||||
basepython = python2.7
|
||||
commands = python setup.py build_sphinx
|
||||
deps = -r{toxinidir}/doc/requirements.txt
|
||||
commands = sphinx-build -W -b html doc/source doc/build/html
|
||||
|
||||
[testenv:api-ref]
|
||||
# This environment is called from CI scripts to test and publish
|
||||
# the API Ref to developer.openstack.org.
|
||||
# we do not use -W here because we are doing some slightly tricky
|
||||
# things to build a single page document, and as such, we are ok
|
||||
# ignoring the duplicate stanzas warning.
|
||||
basepython = python2.7
|
||||
deps = -r{toxinidir}/doc/requirements.txt
|
||||
commands =
|
||||
rm -rf api-ref/build
|
||||
sphinx-build -W -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html
|
||||
|
@ -132,4 +128,5 @@ deps = bindep
|
|||
commands = bindep test
|
||||
|
||||
[testenv:releasenotes]
|
||||
deps = -r{toxinidir}/doc/requirements.txt
|
||||
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
|
Loading…
Reference in New Issue