Merge remote-tracking branch 'remotes/origin/master' into merge-master

Conflicts resolved:
	.zuul.yaml
	requirements.txt
	swift/obj/replicator.py

Change-Id: I8c988890173f8e038f3285ac63537f5bf8698937
This commit is contained in:
Kota Tsuyuzaki 2019-04-25 17:18:43 +09:00
commit d391957fd7
43 changed files with 407 additions and 325 deletions

View File

@ -331,6 +331,7 @@
irrelevant-files: irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$ - ^(api-ref|doc|releasenotes)/.*$
- ^test/(functional|probe)/.*$ - ^test/(functional|probe)/.*$
voting: false
- swift-tox-py36: - swift-tox-py36:
irrelevant-files: irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$ - ^(api-ref|doc|releasenotes)/.*$
@ -340,7 +341,6 @@
irrelevant-files: irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$ - ^(api-ref|doc|releasenotes)/.*$
- ^test/(functional|probe)/.*$ - ^test/(functional|probe)/.*$
voting: false
- swift-tox-func: - swift-tox-func:
irrelevant-files: irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$ - ^(api-ref|doc|releasenotes)/.*$
@ -412,7 +412,7 @@
# long-running jobs, like probetests or (once they move to # long-running jobs, like probetests or (once they move to
# in-tree definitions) dsvm jobs. # in-tree definitions) dsvm jobs.
- swift-tox-py27 - swift-tox-py27
- swift-tox-py35 - swift-tox-py37
- swift-tox-func - swift-tox-func
- swift-tox-func-encryption - swift-tox-func-encryption
- swift-tox-func-domain-remap-staticweb - swift-tox-func-domain-remap-staticweb

View File

@ -1888,7 +1888,7 @@ swift (1.13.1, OpenStack Icehouse)
A new proxy config variable (strict_cors_mode, default to True) A new proxy config variable (strict_cors_mode, default to True)
has been added. Setting it to False keeps the old behavior. For has been added. Setting it to False keeps the old behavior. For
an overview of old versus new behavior, please see an overview of old versus new behavior, please see
https://review.openstack.org/#/c/69419/ https://review.opendev.org/#/c/69419/
* Invert the responsibility of the two instances of proxy-logging in * Invert the responsibility of the two instances of proxy-logging in
the proxy pipeline the proxy pipeline

View File

@ -52,7 +52,7 @@ Reviewing Someone Else's Code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
All code reviews in OpenStack projects are done on All code reviews in OpenStack projects are done on
https://review.openstack.org/. Reviewing patches is one of the most effective https://review.opendev.org/. Reviewing patches is one of the most effective
ways you can contribute to the community. ways you can contribute to the community.
We've written REVIEW_GUIDELINES.rst (found in this source tree) to help you We've written REVIEW_GUIDELINES.rst (found in this source tree) to help you

View File

@ -162,6 +162,11 @@ try:
except OSError: except OSError:
warnings.warn('Cannot get last updated time from git repository. ' warnings.warn('Cannot get last updated time from git repository. '
'Not setting "html_last_updated_fmt".') 'Not setting "html_last_updated_fmt".')
else:
if not isinstance(html_last_updated_fmt, str):
# for py3
html_last_updated_fmt = html_last_updated_fmt.decode('ascii')
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.

View File

@ -1109,6 +1109,8 @@ multipart-manifest_put:
description: | description: |
If you include the ``multipart-manifest=put`` query parameter, the object If you include the ``multipart-manifest=put`` query parameter, the object
is a static large object manifest and the body contains the manifest. is a static large object manifest and the body contains the manifest.
See `Static large objects <https://docs.openstack.org/swift/latest
/api/large_objects.html#static-large-objects>`_ for more information.
in: query in: query
required: false required: false
type: string type: string

View File

@ -2252,7 +2252,7 @@ For distros with more recent kernels (for example Ubuntu 12.04 Precise),
we recommend using the default settings (including the default inode size we recommend using the default settings (including the default inode size
of 256 bytes) when creating the file system:: of 256 bytes) when creating the file system::
mkfs.xfs /dev/sda1 mkfs.xfs -L D1 /dev/sda1
In the last couple of years, XFS has made great improvements in how inodes In the last couple of years, XFS has made great improvements in how inodes
are allocated and used. Using the default inode size no longer has an are allocated and used. Using the default inode size no longer has an
@ -2262,7 +2262,7 @@ For distros with older kernels (for example Ubuntu 10.04 Lucid),
some settings can dramatically impact performance. We recommend the some settings can dramatically impact performance. We recommend the
following when creating the file system:: following when creating the file system::
mkfs.xfs -i size=1024 /dev/sda1 mkfs.xfs -i size=1024 -L D1 /dev/sda1
Setting the inode size is important, as XFS stores xattr data in the inode. Setting the inode size is important, as XFS stores xattr data in the inode.
If the metadata is too large to fit in the inode, a new extent is created, If the metadata is too large to fit in the inode, a new extent is created,
@ -2272,15 +2272,15 @@ headroom.
The following example mount options are recommended when using XFS:: The following example mount options are recommended when using XFS::
mount -t xfs -o noatime,nodiratime,nobarrier,logbufs=8 /dev/sda1 /srv/node/sda mount -t xfs -o noatime,nodiratime,nobarrier,logbufs=8 -L D1 /srv/node/d1
We do not recommend running Swift on RAID, but if you are using We do not recommend running Swift on RAID, but if you are using
RAID it is also important to make sure that the proper sunit and swidth RAID it is also important to make sure that the proper sunit and swidth
settings get set so that XFS can make most efficient use of the RAID array. settings get set so that XFS can make most efficient use of the RAID array.
For a standard Swift install, all data drives are mounted directly under For a standard Swift install, all data drives are mounted directly under
``/srv/node`` (as can be seen in the above example of mounting ``/dev/sda1`` as ``/srv/node`` (as can be seen in the above example of mounting label ``D1``
``/srv/node/sda``). If you choose to mount the drives in another directory, as ``/srv/node/d1``). If you choose to mount the drives in another directory,
be sure to set the `devices` config option in all of the server configs to be sure to set the `devices` config option in all of the server configs to
point to the correct directory. point to the correct directory.
@ -2322,7 +2322,7 @@ The following settings should be in `/etc/sysctl.conf`::
# double amount of allowed conntrack # double amount of allowed conntrack
net.ipv4.netfilter.ip_conntrack_max = 262144 net.ipv4.netfilter.ip_conntrack_max = 262144
To load the updated sysctl settings, run ``sudo sysctl -p`` To load the updated sysctl settings, run ``sudo sysctl -p``.
A note about changing the TIME_WAIT values. By default the OS will hold A note about changing the TIME_WAIT values. By default the OS will hold
a port open for 60 seconds to ensure that any remaining packets can be a port open for 60 seconds to ensure that any remaining packets can be

View File

@ -117,7 +117,7 @@ Tracking your changes
--------------------- ---------------------
After proposing changes to Swift, you can track them at After proposing changes to Swift, you can track them at
https://review.openstack.org. After logging in, you will see a dashboard of https://review.opendev.org. After logging in, you will see a dashboard of
"Outgoing reviews" for changes you have proposed, "Incoming reviews" for "Outgoing reviews" for changes you have proposed, "Incoming reviews" for
changes you are reviewing, and "Recently closed" changes for which you were changes you are reviewing, and "Recently closed" changes for which you were
either a reviewer or owner. either a reviewer or owner.

View File

@ -47,4 +47,4 @@ Install and configure components
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/proxy-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/proxy-server.conf-sample?h=stable/queens # curl -o /etc/swift/proxy-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/proxy-server.conf-sample

View File

@ -45,6 +45,6 @@ Install and configure components
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/proxy-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/proxy-server.conf-sample?h=stable/queens # curl -o /etc/swift/proxy-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/proxy-server.conf-sample
3. .. include:: controller-include.txt 3. .. include:: controller-include.txt

View File

@ -47,6 +47,6 @@ Install and configure components
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/proxy-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/proxy-server.conf-sample?h=stable/queens # curl -o /etc/swift/proxy-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/proxy-server.conf-sample
4. .. include:: controller-include.txt 4. .. include:: controller-include.txt

View File

@ -19,7 +19,7 @@ This section applies to Red Hat Enterprise Linux 7 and CentOS 7.
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/swift.conf \ # curl -o /etc/swift/swift.conf \
https://git.openstack.org/cgit/openstack/swift/plain/etc/swift.conf-sample?h=stable/queens https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/swift.conf-sample
#. Edit the ``/etc/swift/swift.conf`` file and complete the following #. Edit the ``/etc/swift/swift.conf`` file and complete the following
actions: actions:

View File

@ -19,7 +19,7 @@ This section applies to Ubuntu 14.04 (LTS) and Debian.
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/swift.conf \ # curl -o /etc/swift/swift.conf \
https://git.openstack.org/cgit/openstack/swift/plain/etc/swift.conf-sample?h=stable/queens https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/swift.conf-sample
#. Edit the ``/etc/swift/swift.conf`` file and complete the following #. Edit the ``/etc/swift/swift.conf`` file and complete the following
actions: actions:

View File

@ -133,9 +133,9 @@ Install and configure components
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/account-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/account-server.conf-sample?h=stable/queens # curl -o /etc/swift/account-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/account-server.conf-sample
# curl -o /etc/swift/container-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/container-server.conf-sample?h=stable/queens # curl -o /etc/swift/container-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/container-server.conf-sample
# curl -o /etc/swift/object-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/object-server.conf-sample?h=stable/queens # curl -o /etc/swift/object-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/object-server.conf-sample
3. .. include:: storage-include1.txt 3. .. include:: storage-include1.txt
4. .. include:: storage-include2.txt 4. .. include:: storage-include2.txt

View File

@ -137,9 +137,9 @@ Install and configure components
.. code-block:: console .. code-block:: console
# curl -o /etc/swift/account-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/account-server.conf-sample?h=stable/queens # curl -o /etc/swift/account-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/account-server.conf-sample
# curl -o /etc/swift/container-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/container-server.conf-sample?h=stable/queens # curl -o /etc/swift/container-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/container-server.conf-sample
# curl -o /etc/swift/object-server.conf https://git.openstack.org/cgit/openstack/swift/plain/etc/object-server.conf-sample?h=stable/queens # curl -o /etc/swift/object-server.conf https://opendev.org/openstack/swift/raw/branch/stable/queens/etc/object-server.conf-sample
3. .. include:: storage-include1.txt 3. .. include:: storage-include1.txt
4. .. include:: storage-include2.txt 4. .. include:: storage-include2.txt

View File

@ -10,7 +10,7 @@ chardet==3.0.4
cliff==2.11.0 cliff==2.11.0
cmd2==0.8.1 cmd2==0.8.1
coverage==3.6 coverage==3.6
cryptography==1.6 cryptography==1.8.2
debtcollector==1.19.0 debtcollector==1.19.0
dnspython==1.14.0 dnspython==1.14.0
docutils==0.11 docutils==0.11
@ -40,7 +40,7 @@ mock==2.0
monotonic==1.4 monotonic==1.4
msgpack==0.5.6 msgpack==0.5.6
netaddr==0.7.19 netaddr==0.7.19
netifaces==0.5 netifaces==0.8
nose==1.3.7 nose==1.3.7
nosehtmloutput==0.0.3 nosehtmloutput==0.0.3
nosexcover==1.0.10 nosexcover==1.0.10
@ -49,6 +49,7 @@ os-api-ref==1.0.0
os-testr==0.8.0 os-testr==0.8.0
oslo.config==4.0.0 oslo.config==4.0.0
oslo.i18n==3.20.0 oslo.i18n==3.20.0
oslo.log==3.22.0
oslo.serialization==2.25.0 oslo.serialization==2.25.0
oslo.utils==3.36.0 oslo.utils==3.36.0
PasteDeploy==1.3.3 PasteDeploy==1.3.3

View File

@ -2,18 +2,18 @@
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
dnspython>=1.14.0;python_version=='2.7' # http://www.dnspython.org/LICENSE dnspython>=1.14.0;python_version=='2.7' # http://www.dnspython.org/LICENSE
eventlet>=0.17.4,!=0.23.0 # MIT eventlet>=0.17.4,!=0.23.0 # MIT
greenlet>=0.3.1 greenlet>=0.3.1
netifaces>=0.5,!=0.10.0,!=0.10.1 netifaces>=0.8,!=0.10.0,!=0.10.1
PasteDeploy>=1.3.3 PasteDeploy>=1.3.3
lxml>=3.4.1 lxml>=3.4.1
requests>=2.14.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0
six>=1.9.0 six>=1.9.0
xattr>=0.4 xattr>=0.4;sys_platform!='win32' # MIT
PyECLib>=1.3.1 # BSD PyECLib>=1.3.1 # BSD
cryptography!=2.0,>=1.6 # BSD/Apache-2.0 cryptography!=2.0,>=1.8.2 # BSD/Apache-2.0
ipaddress>=1.0.16;python_version<'3.3' # PSF ipaddress>=1.0.16;python_version<'3.3' # PSF
# grpcio will fail to work with eventlet starting with 1.3.5. # grpcio will fail to work with eventlet starting with 1.3.5.
# see this for a similar issue with gevent: https://github.com/grpc/grpc/issues/4629 and https://github.com/gevent/gevent/issues/786 # see this for a similar issue with gevent: https://github.com/grpc/grpc/issues/4629 and https://github.com/gevent/gevent/issues/786
# don't use eventlet for the object-server ? # don't use eventlet for the object-server ?

View File

@ -15,9 +15,7 @@
import json import json
import six import six
from six.moves.urllib.parse import unquote from six.moves.urllib.parse import unquote, urlparse
from swift.common.utils import urlparse
def clean_acl(name, value): def clean_acl(name, value):

View File

@ -121,14 +121,14 @@ Here's an example using ``curl`` with tiny 1-byte segments::
import json import json
import six import six
from six.moves.urllib.parse import unquote
from hashlib import md5 from hashlib import md5
from swift.common import constraints from swift.common import constraints
from swift.common.exceptions import ListingIterError, SegmentError from swift.common.exceptions import ListingIterError, SegmentError
from swift.common.http import is_success from swift.common.http import is_success
from swift.common.swob import Request, Response, \ from swift.common.swob import Request, Response, \
HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict, \
str_to_wsgi, wsgi_to_str, wsgi_quote, wsgi_unquote
from swift.common.utils import get_logger, \ from swift.common.utils import get_logger, \
RateLimitedIterator, quote, close_if_possible, closing_if_possible RateLimitedIterator, quote, close_if_possible, closing_if_possible
from swift.common.request_helpers import SegmentedIterable from swift.common.request_helpers import SegmentedIterable
@ -143,9 +143,18 @@ class GetContext(WSGIContext):
def _get_container_listing(self, req, version, account, container, def _get_container_listing(self, req, version, account, container,
prefix, marker=''): prefix, marker=''):
'''
:param version: whatever
:param account: native
:param container: native
:param prefix: native
:param marker: native
'''
con_req = make_subrequest( con_req = make_subrequest(
req.environ, req.environ,
path=quote('/'.join(['', version, account, container])), path=wsgi_quote('/'.join([
'', str_to_wsgi(version),
str_to_wsgi(account), str_to_wsgi(container)])),
method='GET', method='GET',
headers={'x-auth-token': req.headers.get('x-auth-token')}, headers={'x-auth-token': req.headers.get('x-auth-token')},
agent=('%(orig)s ' + 'DLO MultipartGET'), swift_source='DLO') agent=('%(orig)s ' + 'DLO MultipartGET'), swift_source='DLO')
@ -156,14 +165,24 @@ class GetContext(WSGIContext):
con_resp = con_req.get_response(self.dlo.app) con_resp = con_req.get_response(self.dlo.app)
if not is_success(con_resp.status_int): if not is_success(con_resp.status_int):
if req.method == 'HEAD': if req.method == 'HEAD':
con_resp.body = '' con_resp.body = b''
return con_resp, None return con_resp, None
with closing_if_possible(con_resp.app_iter): with closing_if_possible(con_resp.app_iter):
return None, json.loads(''.join(con_resp.app_iter)) return None, json.loads(b''.join(con_resp.app_iter))
def _segment_listing_iterator(self, req, version, account, container, def _segment_listing_iterator(self, req, version, account, container,
prefix, segments, first_byte=None, prefix, segments, first_byte=None,
last_byte=None): last_byte=None):
'''
:param req: upstream request
:param version: native
:param account: native
:param container: native
:param prefix: native
:param segments: array of dicts, with native strings
:param first_byte: number
:param last_byte: number
'''
# It's sort of hokey that this thing takes in the first page of # It's sort of hokey that this thing takes in the first page of
# segments as an argument, but we need to compute the etag and content # segments as an argument, but we need to compute the etag and content
# length from the first page, and it's better to have a hokey # length from the first page, and it's better to have a hokey
@ -173,7 +192,6 @@ class GetContext(WSGIContext):
if last_byte is None: if last_byte is None:
last_byte = float("inf") last_byte = float("inf")
marker = ''
while True: while True:
for segment in segments: for segment in segments:
seg_length = int(segment['bytes']) seg_length = int(segment['bytes'])
@ -188,7 +206,7 @@ class GetContext(WSGIContext):
break break
seg_name = segment['name'] seg_name = segment['name']
if isinstance(seg_name, six.text_type): if six.PY2:
seg_name = seg_name.encode("utf-8") seg_name = seg_name.encode("utf-8")
# We deliberately omit the etag and size here; # We deliberately omit the etag and size here;
@ -227,16 +245,18 @@ class GetContext(WSGIContext):
"Got status %d listing container /%s/%s" % "Got status %d listing container /%s/%s" %
(error_response.status_int, account, container)) (error_response.status_int, account, container))
def get_or_head_response(self, req, x_object_manifest, def get_or_head_response(self, req, x_object_manifest):
response_headers=None): '''
if response_headers is None: :param req: user's request
response_headers = self._response_headers :param x_object_manifest: as unquoted, native string
'''
response_headers = self._response_headers
container, obj_prefix = x_object_manifest.split('/', 1) container, obj_prefix = x_object_manifest.split('/', 1)
container = unquote(container)
obj_prefix = unquote(obj_prefix)
version, account, _junk = req.split_path(2, 3, True) version, account, _junk = req.split_path(2, 3, True)
version = wsgi_to_str(version)
account = wsgi_to_str(account)
error_response, segments = self._get_container_listing( error_response, segments = self._get_container_listing(
req, version, account, container, obj_prefix) req, version, account, container, obj_prefix)
if error_response: if error_response:
@ -311,7 +331,7 @@ class GetContext(WSGIContext):
if h.lower() != "etag"] if h.lower() != "etag"]
etag = md5() etag = md5()
for seg_dict in segments: for seg_dict in segments:
etag.update(seg_dict['hash'].strip('"')) etag.update(seg_dict['hash'].strip('"').encode('utf8'))
response_headers.append(('Etag', '"%s"' % etag.hexdigest())) response_headers.append(('Etag', '"%s"' % etag.hexdigest()))
app_iter = None app_iter = None
@ -353,7 +373,8 @@ class GetContext(WSGIContext):
for header, value in self._response_headers: for header, value in self._response_headers:
if (header.lower() == 'x-object-manifest'): if (header.lower() == 'x-object-manifest'):
close_if_possible(resp_iter) close_if_possible(resp_iter)
response = self.get_or_head_response(req, value) response = self.get_or_head_response(
req, wsgi_to_str(wsgi_unquote(value)))
return response(req.environ, start_response) return response(req.environ, start_response)
# Not a dynamic large object manifest; just pass it through. # Not a dynamic large object manifest; just pass it through.
start_response(self._response_status, start_response(self._response_status,

View File

@ -90,6 +90,8 @@ def _header_strip(value):
# behave as though it wasn't provided # behave as though it wasn't provided
return None return None
return stripped return stripped
_header_strip.re = re.compile('^[\x00-\x20]*|[\x00-\x20]*$') _header_strip.re = re.compile('^[\x00-\x20]*|[\x00-\x20]*$')
@ -1427,8 +1429,10 @@ class S3Request(swob.Request):
else: else:
# otherwise we do naive HEAD request with the authentication # otherwise we do naive HEAD request with the authentication
resp = self.get_response(app, 'HEAD', self.container_name, '') resp = self.get_response(app, 'HEAD', self.container_name, '')
headers = resp.sw_headers.copy()
headers.update(resp.sysmeta_headers)
return headers_to_container_info( return headers_to_container_info(
resp.sw_headers, resp.status_int) # pylint: disable-msg=E1101 headers, resp.status_int) # pylint: disable-msg=E1101
def gen_multipart_manifest_delete_query(self, app, obj=None): def gen_multipart_manifest_delete_query(self, app, obj=None):
if not self.allow_multipart_uploads: if not self.allow_multipart_uploads:

View File

@ -80,7 +80,7 @@ class S3Response(S3ResponseBase, swob.Response):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
swob.Response.__init__(self, *args, **kwargs) swob.Response.__init__(self, *args, **kwargs)
sw_sysmeta_headers = swob.HeaderKeyDict() s3_sysmeta_headers = swob.HeaderKeyDict()
sw_headers = swob.HeaderKeyDict() sw_headers = swob.HeaderKeyDict()
headers = HeaderKeyDict() headers = HeaderKeyDict()
self.is_slo = False self.is_slo = False
@ -103,12 +103,14 @@ class S3Response(S3ResponseBase, swob.Response):
key = sysmeta_prefix(_server_type) + \ key = sysmeta_prefix(_server_type) + \
key[len('x-%s-sysmeta-swift3-' % _server_type):] key[len('x-%s-sysmeta-swift3-' % _server_type):]
if key not in sw_sysmeta_headers: if key not in s3_sysmeta_headers:
# To avoid overwrite s3api sysmeta by older swift3 # To avoid overwrite s3api sysmeta by older swift3
# sysmeta set the key only when the key does not exist # sysmeta set the key only when the key does not exist
sw_sysmeta_headers[key] = val s3_sysmeta_headers[key] = val
elif is_s3api_sysmeta(key, _server_type): elif is_s3api_sysmeta(key, _server_type):
sw_sysmeta_headers[key] = val s3_sysmeta_headers[key] = val
else:
sw_headers[key] = val
else: else:
sw_headers[key] = val sw_headers[key] = val
@ -132,7 +134,7 @@ class S3Response(S3ResponseBase, swob.Response):
self.is_slo = config_true_value(val) self.is_slo = config_true_value(val)
# Check whether we stored the AWS-style etag on upload # Check whether we stored the AWS-style etag on upload
override_etag = sw_sysmeta_headers.get( override_etag = s3_sysmeta_headers.get(
sysmeta_header('object', 'etag')) sysmeta_header('object', 'etag'))
if override_etag is not None: if override_etag is not None:
# Multipart uploads in AWS have ETags like # Multipart uploads in AWS have ETags like
@ -153,7 +155,7 @@ class S3Response(S3ResponseBase, swob.Response):
# Used for pure swift header handling at the request layer # Used for pure swift header handling at the request layer
self.sw_headers = sw_headers self.sw_headers = sw_headers
self.sysmeta_headers = sw_sysmeta_headers self.sysmeta_headers = s3_sysmeta_headers
@classmethod @classmethod
def from_swift_resp(cls, sw_resp): def from_swift_resp(cls, sw_resp):

View File

@ -67,7 +67,7 @@ from six.moves import urllib
from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \ from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
HTTPException HTTPException
from swift.common.utils import config_true_value, split_path, get_logger, \ from swift.common.utils import config_true_value, split_path, get_logger, \
cache_from_env cache_from_env, append_underscore
from swift.common.wsgi import ConfigFileError from swift.common.wsgi import ConfigFileError
@ -149,7 +149,8 @@ class S3Token(object):
self._timeout = float(conf.get('http_timeout', '10.0')) self._timeout = float(conf.get('http_timeout', '10.0'))
if not (0 < self._timeout <= 60): if not (0 < self._timeout <= 60):
raise ValueError('http_timeout must be between 0 and 60 seconds') raise ValueError('http_timeout must be between 0 and 60 seconds')
self._reseller_prefix = conf.get('reseller_prefix', 'AUTH_') self._reseller_prefix = append_underscore(
conf.get('reseller_prefix', 'AUTH'))
self._delay_auth_decision = config_true_value( self._delay_auth_decision = config_true_value(
conf.get('delay_auth_decision')) conf.get('delay_auth_decision'))
@ -274,7 +275,9 @@ class S3Token(object):
string_to_sign = s3_auth_details['string_to_sign'] string_to_sign = s3_auth_details['string_to_sign']
if isinstance(string_to_sign, six.text_type): if isinstance(string_to_sign, six.text_type):
string_to_sign = string_to_sign.encode('utf-8') string_to_sign = string_to_sign.encode('utf-8')
token = base64.urlsafe_b64encode(string_to_sign).encode('ascii') token = base64.urlsafe_b64encode(string_to_sign)
if isinstance(token, six.binary_type):
token = token.decode('ascii')
# NOTE(chmou): This is to handle the special case with nova # NOTE(chmou): This is to handle the special case with nova
# when we have the option s3_affix_tenant. We will force it to # when we have the option s3_affix_tenant. We will force it to

View File

@ -125,14 +125,17 @@ Example usage of this middleware via ``swift``:
import cgi import cgi
import json import json
import six
import time import time
from six.moves.urllib.parse import urlparse
from swift.common.utils import human_readable, split_path, config_true_value, \ from swift.common.utils import human_readable, split_path, config_true_value, \
quote, register_swift_info, get_logger, urlparse quote, register_swift_info, get_logger
from swift.common.wsgi import make_env, WSGIContext from swift.common.wsgi import make_env, WSGIContext
from swift.common.http import is_success, is_redirection, HTTP_NOT_FOUND from swift.common.http import is_success, is_redirection, HTTP_NOT_FOUND
from swift.common.swob import Response, HTTPMovedPermanently, HTTPNotFound, \ from swift.common.swob import Response, HTTPMovedPermanently, HTTPNotFound, \
Request Request, wsgi_quote, wsgi_to_str
from swift.proxy.controllers.base import get_container_info from swift.proxy.controllers.base import get_container_info
@ -145,6 +148,12 @@ class _StaticWebContext(WSGIContext):
that might need to be handled to make keeping contextual that might need to be handled to make keeping contextual
information about the request a bit simpler than storing it in information about the request a bit simpler than storing it in
the WSGI env. the WSGI env.
:param staticweb: The staticweb middleware object in use.
:param version: A WSGI string representation of the swift api version.
:param account: A WSGI string representation of the account name.
:param container: A WSGI string representation of the container name.
:param obj: A WSGI string representation of the object name.
""" """
def __init__(self, staticweb, version, account, container, obj): def __init__(self, staticweb, version, account, container, obj):
@ -223,9 +232,9 @@ class _StaticWebContext(WSGIContext):
:param start_response: The original WSGI start_response hook. :param start_response: The original WSGI start_response hook.
:param prefix: Any prefix desired for the container listing. :param prefix: Any prefix desired for the container listing.
""" """
label = env['PATH_INFO'] label = wsgi_to_str(env['PATH_INFO'])
if self._listings_label: if self._listings_label:
groups = env['PATH_INFO'].split('/') groups = wsgi_to_str(env['PATH_INFO']).split('/')
label = '{0}/{1}'.format(self._listings_label, label = '{0}/{1}'.format(self._listings_label,
'/'.join(groups[4:])) '/'.join(groups[4:]))
@ -262,14 +271,14 @@ class _StaticWebContext(WSGIContext):
self.agent, swift_source='SW') self.agent, swift_source='SW')
tmp_env['QUERY_STRING'] = 'delimiter=/' tmp_env['QUERY_STRING'] = 'delimiter=/'
if prefix: if prefix:
tmp_env['QUERY_STRING'] += '&prefix=%s' % quote(prefix) tmp_env['QUERY_STRING'] += '&prefix=%s' % wsgi_quote(prefix)
else: else:
prefix = '' prefix = ''
resp = self._app_call(tmp_env) resp = self._app_call(tmp_env)
if not is_success(self._get_status_int()): if not is_success(self._get_status_int()):
return self._error_response(resp, env, start_response) return self._error_response(resp, env, start_response)
listing = None listing = None
body = ''.join(resp) body = b''.join(resp)
if body: if body:
listing = json.loads(body) listing = json.loads(body)
if not listing: if not listing:
@ -280,7 +289,8 @@ class _StaticWebContext(WSGIContext):
'Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' \ 'Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' \
'<html>\n' \ '<html>\n' \
' <head>\n' \ ' <head>\n' \
' <title>Listing of %s</title>\n' % cgi.escape(label) ' <title>Listing of %s</title>\n' % \
cgi.escape(label)
if self._listings_css: if self._listings_css:
body += ' <link rel="stylesheet" type="text/css" ' \ body += ' <link rel="stylesheet" type="text/css" ' \
'href="%s" />\n' % (self._build_css_path(prefix)) 'href="%s" />\n' % (self._build_css_path(prefix))
@ -308,7 +318,8 @@ class _StaticWebContext(WSGIContext):
' </tr>\n' ' </tr>\n'
for item in listing: for item in listing:
if 'subdir' in item: if 'subdir' in item:
subdir = item['subdir'].encode("utf-8") subdir = item['subdir'] if six.PY3 else \
item['subdir'].encode('utf-8')
if prefix: if prefix:
subdir = subdir[len(prefix):] subdir = subdir[len(prefix):]
body += ' <tr class="item subdir">\n' \ body += ' <tr class="item subdir">\n' \
@ -319,13 +330,16 @@ class _StaticWebContext(WSGIContext):
(quote(subdir), cgi.escape(subdir)) (quote(subdir), cgi.escape(subdir))
for item in listing: for item in listing:
if 'name' in item: if 'name' in item:
name = item['name'].encode("utf-8") name = item['name'] if six.PY3 else \
item['name'].encode('utf-8')
if prefix: if prefix:
name = name[len(prefix):] name = name[len(prefix):]
content_type = item['content_type'].encode("utf-8") content_type = item['content_type'] if six.PY3 else \
item['content_type'].encode('utf-8')
bytes = human_readable(item['bytes']) bytes = human_readable(item['bytes'])
last_modified = ( last_modified = (
cgi.escape(item['last_modified'].encode("utf-8")). cgi.escape(item['last_modified'] if six.PY3 else
item['last_modified'].encode('utf-8')).
split('.')[0].replace('T', ' ')) split('.')[0].replace('T', ' '))
body += ' <tr class="item %s">\n' \ body += ' <tr class="item %s">\n' \
' <td class="colname"><a href="%s">%s</a></td>\n' \ ' <td class="colname"><a href="%s">%s</a></td>\n' \
@ -362,7 +376,8 @@ class _StaticWebContext(WSGIContext):
env['wsgi.url_scheme'] = self.url_scheme env['wsgi.url_scheme'] = self.url_scheme
if self.url_host: if self.url_host:
env['HTTP_HOST'] = self.url_host env['HTTP_HOST'] = self.url_host
resp = HTTPMovedPermanently(location=(env['PATH_INFO'] + '/')) resp = HTTPMovedPermanently(
location=wsgi_quote(env['PATH_INFO'] + '/'))
return resp(env, start_response) return resp(env, start_response)
def handle_container(self, env, start_response): def handle_container(self, env, start_response):
@ -466,9 +481,9 @@ class _StaticWebContext(WSGIContext):
self.version, self.account, self.container), self.version, self.account, self.container),
self.agent, swift_source='SW') self.agent, swift_source='SW')
tmp_env['QUERY_STRING'] = 'limit=1&delimiter=/&prefix=%s' % ( tmp_env['QUERY_STRING'] = 'limit=1&delimiter=/&prefix=%s' % (
quote(self.obj + '/'), ) quote(wsgi_to_str(self.obj) + '/'), )
resp = self._app_call(tmp_env) resp = self._app_call(tmp_env)
body = ''.join(resp) body = b''.join(resp)
if not is_success(self._get_status_int()) or not body or \ if not is_success(self._get_status_int()) or not body or \
not json.loads(body): not json.loads(body):
resp = HTTPNotFound()(env, self._start_response) resp = HTTPNotFound()(env, self._start_response)

View File

@ -161,7 +161,7 @@ from cgi import parse_header
from six.moves.urllib.parse import unquote from six.moves.urllib.parse import unquote
from swift.common.utils import get_logger, register_swift_info, split_path, \ from swift.common.utils import get_logger, register_swift_info, split_path, \
MD5_OF_EMPTY_STRING, closing_if_possible MD5_OF_EMPTY_STRING, closing_if_possible, quote
from swift.common.constraints import check_account_format from swift.common.constraints import check_account_format
from swift.common.wsgi import WSGIContext, make_subrequest from swift.common.wsgi import WSGIContext, make_subrequest
from swift.common.request_helpers import get_sys_meta_prefix, \ from swift.common.request_helpers import get_sys_meta_prefix, \
@ -208,6 +208,7 @@ def _check_symlink_header(req):
req, TGT_OBJ_SYMLINK_HDR, 2, req, TGT_OBJ_SYMLINK_HDR, 2,
'X-Symlink-Target header must be of the ' 'X-Symlink-Target header must be of the '
'form <container name>/<object name>') 'form <container name>/<object name>')
req.headers[TGT_OBJ_SYMLINK_HDR] = quote('%s/%s' % (container, obj))
# Check account format if it exists # Check account format if it exists
account = check_account_format( account = check_account_format(
@ -217,7 +218,9 @@ def _check_symlink_header(req):
# Extract request path # Extract request path
_junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True) _junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True)
if not account: if account:
req.headers[TGT_ACCT_SYMLINK_HDR] = quote(account)
else:
account = req_acc account = req_acc
# Check if symlink targets the symlink itself or not # Check if symlink targets the symlink itself or not
@ -378,9 +381,9 @@ class SymlinkObjectContext(WSGIContext):
:returns: new request for target path if it's symlink otherwise :returns: new request for target path if it's symlink otherwise
None None
""" """
version, account, _junk = split_path(req.path, 2, 3, True) version, account, _junk = req.split_path(2, 3, True)
account = self._response_header_value( account = self._response_header_value(
TGT_ACCT_SYSMETA_SYMLINK_HDR) or account TGT_ACCT_SYSMETA_SYMLINK_HDR) or quote(account)
target_path = os.path.join( target_path = os.path.join(
'/', version, account, '/', version, account,
symlink_target.lstrip('/')) symlink_target.lstrip('/'))
@ -485,7 +488,7 @@ class SymlinkObjectContext(WSGIContext):
if tgt_co: if tgt_co:
version, account, _junk = req.split_path(2, 3, True) version, account, _junk = req.split_path(2, 3, True)
target_acc = self._response_header_value( target_acc = self._response_header_value(
TGT_ACCT_SYSMETA_SYMLINK_HDR) or account TGT_ACCT_SYSMETA_SYMLINK_HDR) or quote(account)
location_hdr = os.path.join( location_hdr = os.path.join(
'/', version, target_acc, tgt_co) '/', version, target_acc, tgt_co)
req.environ['swift.leave_relative_location'] = True req.environ['swift.leave_relative_location'] = True

View File

@ -75,9 +75,8 @@ from six.moves import cPickle as pickle
from six.moves.configparser import (ConfigParser, NoSectionError, from six.moves.configparser import (ConfigParser, NoSectionError,
NoOptionError, RawConfigParser) NoOptionError, RawConfigParser)
from six.moves import range, http_client from six.moves import range, http_client
from six.moves.urllib.parse import ParseResult
from six.moves.urllib.parse import quote as _quote from six.moves.urllib.parse import quote as _quote
from six.moves.urllib.parse import urlparse as stdlib_urlparse from six.moves.urllib.parse import urlparse
from swift import gettext_ as _ from swift import gettext_ as _
import swift.common.exceptions import swift.common.exceptions
@ -2265,7 +2264,7 @@ def get_hub():
Note about epoll: Note about epoll:
Review: https://review.openstack.org/#/c/18806/ Review: https://review.opendev.org/#/c/18806/
There was a problem where once out of every 30 quadrillion There was a problem where once out of every 30 quadrillion
connections, a coroutine wouldn't wake up when the client connections, a coroutine wouldn't wake up when the client
@ -3244,38 +3243,6 @@ class StreamingPile(GreenAsyncPile):
self.pool.__exit__(type, value, traceback) self.pool.__exit__(type, value, traceback)
class ModifiedParseResult(ParseResult):
"""Parse results class for urlparse."""
@property
def hostname(self):
netloc = self.netloc.split('@', 1)[-1]
if netloc.startswith('['):
return netloc[1:].split(']')[0]
elif ':' in netloc:
return netloc.rsplit(':')[0]
return netloc
@property
def port(self):
netloc = self.netloc.split('@', 1)[-1]
if netloc.startswith('['):
netloc = netloc.rsplit(']')[1]
if ':' in netloc:
return int(netloc.rsplit(':')[1])
return None
def urlparse(url):
"""
urlparse augmentation.
This is necessary because urlparse can't handle RFC 2732 URLs.
:param url: URL to parse.
"""
return ModifiedParseResult(*stdlib_urlparse(url))
def validate_sync_to(value, allowed_sync_hosts, realms_conf): def validate_sync_to(value, allowed_sync_hosts, realms_conf):
""" """
Validates an X-Container-Sync-To header value, returning the Validates an X-Container-Sync-To header value, returning the

View File

@ -430,6 +430,7 @@ class SwiftHttpProtocol(wsgi.HttpProtocol):
# for py3: # for py3:
def get_default_type(self): def get_default_type(self):
'''If the client didn't provide a content type, leave it blank.'''
return '' return ''

View File

@ -365,8 +365,9 @@ class ContainerSharder(ContainerReplicator):
'Swift Container Sharder', 'Swift Container Sharder',
request_tries, request_tries,
allow_modify_pipeline=False) allow_modify_pipeline=False)
except IOError as err: except (OSError, IOError) as err:
if err.errno != errno.ENOENT: if err.errno != errno.ENOENT and \
not str(err).endswith(' not found'):
raise raise
raise SystemExit( raise SystemExit(
'Unable to load internal client from config: %r (%s)' % 'Unable to load internal client from config: %r (%s)' %

View File

@ -23,6 +23,7 @@ from random import choice, random
from struct import unpack_from from struct import unpack_from
from eventlet import sleep, Timeout from eventlet import sleep, Timeout
from six.moves.urllib.parse import urlparse
import swift.common.db import swift.common.db
from swift.common.db import DatabaseConnectionError from swift.common.db import DatabaseConnectionError
@ -37,7 +38,7 @@ from swift.common.ring import Ring
from swift.common.ring.utils import is_local_device from swift.common.ring.utils import is_local_device
from swift.common.utils import ( from swift.common.utils import (
clean_content_type, config_true_value, clean_content_type, config_true_value,
FileLikeIter, get_logger, hash_path, quote, urlparse, validate_sync_to, FileLikeIter, get_logger, hash_path, quote, validate_sync_to,
whataremyips, Timestamp, decode_timestamps) whataremyips, Timestamp, decode_timestamps)
from swift.common.daemon import Daemon from swift.common.daemon import Daemon
from swift.common.http import HTTP_UNAUTHORIZED, HTTP_NOT_FOUND from swift.common.http import HTTP_UNAUTHORIZED, HTTP_NOT_FOUND
@ -238,8 +239,9 @@ class ContainerSync(Daemon):
try: try:
self.swift = InternalClient( self.swift = InternalClient(
internal_client_conf, 'Swift Container Sync', request_tries) internal_client_conf, 'Swift Container Sync', request_tries)
except IOError as err: except (OSError, IOError) as err:
if err.errno != errno.ENOENT: if err.errno != errno.ENOENT and \
not str(err).endswith(' not found'):
raise raise
raise SystemExit( raise SystemExit(
_('Unable to load internal client from config: ' _('Unable to load internal client from config: '

View File

@ -818,9 +818,10 @@ class ObjectReplicator(Daemon):
except Exception: except Exception:
self.logger.exception('ERROR creating %s' % obj_path) self.logger.exception('ERROR creating %s' % obj_path)
continue continue
for partition in df_mgr.listdir(obj_path): for partition in df_mgr.listdir(obj_path):
if (override_partitions is not None if (override_partitions is not None and partition.isdigit()
and partition not in override_partitions): and int(partition) not in override_partitions):
continue continue
if (partition.startswith('auditor_status_') and if (partition.startswith('auditor_status_') and

View File

@ -16,8 +16,10 @@
import os import os
import test.functional as tf import test.functional as tf
from boto.s3.connection import S3Connection, OrdinaryCallingFormat, \ from boto.s3.connection import S3Connection, OrdinaryCallingFormat, \
BotoClientError, S3ResponseError S3ResponseError
import six import six
import sys
import traceback
RETRY_COUNT = 3 RETRY_COUNT = 3
@ -92,11 +94,12 @@ class Connection(object):
# 404 means NoSuchBucket, NoSuchKey, or NoSuchUpload # 404 means NoSuchBucket, NoSuchKey, or NoSuchUpload
if e.status != 404: if e.status != 404:
raise raise
except (BotoClientError, S3ResponseError) as e: except Exception as e:
exceptions.append(e) exceptions.append(''.join(
traceback.format_exception(*sys.exc_info())))
if exceptions: if exceptions:
# raise the first exception exceptions.insert(0, 'Too many errors to continue:')
raise exceptions.pop(0) raise Exception('\n========\n'.join(exceptions))
def make_request(self, method, bucket='', obj='', headers=None, body='', def make_request(self, method, bucket='', obj='', headers=None, body='',
query=None): query=None):

View File

@ -16,6 +16,8 @@
import functools import functools
from unittest2 import SkipTest from unittest2 import SkipTest
from six.moves.urllib.parse import unquote
from swift.common.utils import quote
import test.functional as tf import test.functional as tf
from test.functional import cluster_info from test.functional import cluster_info
from test.functional.tests import Utils, Base, BaseEnv from test.functional.tests import Utils, Base, BaseEnv
@ -74,8 +76,8 @@ class TestStaticWebEnv(BaseEnv):
'listings_css', 'listings_css',
'dir/', 'dir/',
'dir/obj', 'dir/obj',
'dir/subdir/', 'dir/some sub%dir/',
'dir/subdir/obj'] 'dir/some sub%dir/obj']
cls.objects = {} cls.objects = {}
for item in sorted(objects): for item in sorted(objects):
@ -168,12 +170,12 @@ class TestStaticWeb(Base):
def _test_redirect_slash_direct(self, anonymous): def _test_redirect_slash_direct(self, anonymous):
host = self.env.account.conn.storage_netloc host = self.env.account.conn.storage_netloc
path = '%s/%s' % (self.env.account.conn.storage_path, path = '%s/%s' % (self.env.account.conn.storage_path,
self.env.container.name) quote(self.env.container.name))
self._test_redirect_with_slash(host, path, anonymous=anonymous) self._test_redirect_with_slash(host, path, anonymous=anonymous)
path = '%s/%s/%s' % (self.env.account.conn.storage_path, path = '%s/%s/%s' % (self.env.account.conn.storage_path,
self.env.container.name, quote(self.env.container.name),
self.env.objects['dir/'].name) quote(self.env.objects['dir/'].name))
self._test_redirect_with_slash(host, path, anonymous=anonymous) self._test_redirect_with_slash(host, path, anonymous=anonymous)
def test_redirect_slash_auth_direct(self): def test_redirect_slash_auth_direct(self):
@ -185,10 +187,10 @@ class TestStaticWeb(Base):
@requires_domain_remap @requires_domain_remap
def _test_redirect_slash_remap_acct(self, anonymous): def _test_redirect_slash_remap_acct(self, anonymous):
host = self.domain_remap_acct host = self.domain_remap_acct
path = '/%s' % self.env.container.name path = '/%s' % quote(self.env.container.name)
self._test_redirect_with_slash(host, path, anonymous=anonymous) self._test_redirect_with_slash(host, path, anonymous=anonymous)
path = '/%s/%s' % (self.env.container.name, path = '/%s/%s' % (quote(self.env.container.name),
self.env.objects['dir/'].name) self.env.objects['dir/'].name)
self._test_redirect_with_slash(host, path, anonymous=anonymous) self._test_redirect_with_slash(host, path, anonymous=anonymous)
@ -229,13 +231,14 @@ class TestStaticWeb(Base):
self._set_staticweb_headers(listings=True, self._set_staticweb_headers(listings=True,
listings_css=(css is not None)) listings_css=(css is not None))
if title is None: if title is None:
title = path title = unquote(path)
expected_in = ['Listing of %s' % title] + [ expected_in = ['Listing of %s' % title] + [
'<a href="{0}">{0}</a>'.format(link) for link in links] '<a href="{0}">{1}</a>'.format(quote(link), link)
for link in links]
expected_not_in = notins expected_not_in = notins
if css: if css:
expected_in.append('<link rel="stylesheet" type="text/css" ' expected_in.append('<link rel="stylesheet" type="text/css" '
'href="%s" />' % css) 'href="%s" />' % quote(css))
self._test_get_path(host, path, anonymous=anonymous, self._test_get_path(host, path, anonymous=anonymous,
expected_in=expected_in, expected_in=expected_in,
expected_not_in=expected_not_in) expected_not_in=expected_not_in)
@ -244,7 +247,7 @@ class TestStaticWeb(Base):
objects = self.env.objects objects = self.env.objects
host = self.env.account.conn.storage_netloc host = self.env.account.conn.storage_netloc
path = '%s/%s/' % (self.env.account.conn.storage_path, path = '%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name) quote(self.env.container.name))
css = objects['listings_css'].name if listings_css else None css = objects['listings_css'].name if listings_css else None
self._test_listing(host, path, anonymous=True, css=css, self._test_listing(host, path, anonymous=True, css=css,
links=[objects['index'].name, links=[objects['index'].name,
@ -252,15 +255,15 @@ class TestStaticWeb(Base):
notins=[objects['dir/obj'].name]) notins=[objects['dir/obj'].name])
path = '%s/%s/%s/' % (self.env.account.conn.storage_path, path = '%s/%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name, quote(self.env.container.name),
objects['dir/'].name) quote(objects['dir/'].name))
css = '../%s' % objects['listings_css'].name if listings_css else None css = '../%s' % objects['listings_css'].name if listings_css else None
self._test_listing(host, path, anonymous=anonymous, css=css, self._test_listing(
links=[objects['dir/obj'].name.split('/')[-1], host, path, anonymous=anonymous, css=css,
objects['dir/subdir/'].name.split('/')[-1] links=[objects['dir/obj'].name.split('/')[-1],
+ '/'], objects['dir/some sub%dir/'].name.split('/')[-1] + '/'],
notins=[objects['index'].name, notins=[objects['index'].name,
objects['dir/subdir/obj'].name]) objects['dir/some sub%dir/obj'].name])
def test_listing_auth_direct_without_css(self): def test_listing_auth_direct_without_css(self):
self._test_listing_direct(False, False) self._test_listing_direct(False, False)
@ -293,13 +296,12 @@ class TestStaticWeb(Base):
title = '%s/%s/%s/' % (self.env.account.conn.storage_path, title = '%s/%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name, self.env.container.name,
objects['dir/']) objects['dir/'])
self._test_listing(host, path, title=title, anonymous=anonymous, self._test_listing(
css=css, host, path, title=title, anonymous=anonymous, css=css,
links=[objects['dir/obj'].name.split('/')[-1], links=[objects['dir/obj'].name.split('/')[-1],
objects['dir/subdir/'].name.split('/')[-1] objects['dir/some sub%dir/'].name.split('/')[-1] + '/'],
+ '/'], notins=[objects['index'].name,
notins=[objects['index'].name, objects['dir/some sub%dir/obj'].name])
objects['dir/subdir/obj'].name])
def test_listing_auth_remap_acct_without_css(self): def test_listing_auth_remap_acct_without_css(self):
self._test_listing_remap_acct(False, False) self._test_listing_remap_acct(False, False)
@ -332,13 +334,12 @@ class TestStaticWeb(Base):
title = '%s/%s/%s/' % (self.env.account.conn.storage_path, title = '%s/%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name, self.env.container.name,
objects['dir/']) objects['dir/'])
self._test_listing(host, path, title=title, anonymous=anonymous, self._test_listing(
css=css, host, path, title=title, anonymous=anonymous, css=css,
links=[objects['dir/obj'].name.split('/')[-1], links=[objects['dir/obj'].name.split('/')[-1],
objects['dir/subdir/'].name.split('/')[-1] objects['dir/some sub%dir/'].name.split('/')[-1] + '/'],
+ '/'], notins=[objects['index'].name,
notins=[objects['index'].name, objects['dir/some sub%dir/obj'].name])
objects['dir/subdir/obj'].name])
def test_listing_auth_remap_cont_without_css(self): def test_listing_auth_remap_cont_without_css(self):
self._test_listing_remap_cont(False, False) self._test_listing_remap_cont(False, False)
@ -369,12 +370,12 @@ class TestStaticWeb(Base):
objects = self.env.objects objects = self.env.objects
host = self.env.account.conn.storage_netloc host = self.env.account.conn.storage_netloc
path = '%s/%s/' % (self.env.account.conn.storage_path, path = '%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name) quote(self.env.container.name))
self._test_index(host, path, anonymous=anonymous) self._test_index(host, path, anonymous=anonymous)
path = '%s/%s/%s/' % (self.env.account.conn.storage_path, path = '%s/%s/%s/' % (self.env.account.conn.storage_path,
self.env.container.name, quote(self.env.container.name),
objects['dir/'].name) quote(objects['dir/'].name))
self._test_index(host, path, anonymous=anonymous, expected_status=404) self._test_index(host, path, anonymous=anonymous, expected_status=404)
def test_index_auth_direct(self): def test_index_auth_direct(self):

View File

@ -270,23 +270,45 @@ class TestSymlink(Base):
target_obj = 'dealde%2Fl04 011e%204c8df/flash.png' target_obj = 'dealde%2Fl04 011e%204c8df/flash.png'
link_obj = uuid4().hex link_obj = uuid4().hex
# Now let's write a new target object and symlink will be able to # create target using unnormalized path
# return object
resp = retry( resp = retry(
self._make_request, method='PUT', container=self.env.tgt_cont, self._make_request, method='PUT', container=self.env.tgt_cont,
obj=target_obj, body=TARGET_BODY) obj=target_obj, body=TARGET_BODY)
self.assertEqual(resp.status, 201) self.assertEqual(resp.status, 201)
# you can get it using either name
resp = retry(
self._make_request, method='GET', container=self.env.tgt_cont,
obj=target_obj)
self.assertEqual(resp.status, 200)
self.assertEqual(resp.content, TARGET_BODY)
normalized_quoted_obj = 'dealde/l04%20011e%204c8df/flash.png'
self.assertEqual(normalized_quoted_obj, urllib.parse.quote(
urllib.parse.unquote(target_obj)))
resp = retry(
self._make_request, method='GET', container=self.env.tgt_cont,
obj=normalized_quoted_obj)
self.assertEqual(resp.status, 200)
self.assertEqual(resp.content, TARGET_BODY)
# PUT symlink # create a symlink using the un-normalized target path
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj, self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
tgt_cont=self.env.tgt_cont, tgt_cont=self.env.tgt_cont,
tgt_obj=target_obj) tgt_obj=target_obj)
# and it's normalized
self._assertSymlink( self._assertSymlink(
self.env.link_cont, link_obj, self.env.link_cont, link_obj,
expected_content_location="%s/%s" % (self.env.tgt_cont, expected_content_location='%s/%s' % (
target_obj)) self.env.tgt_cont, normalized_quoted_obj))
# create a symlink using the normalized target path
self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj,
tgt_cont=self.env.tgt_cont,
tgt_obj=normalized_quoted_obj)
# and it's ALSO normalized
self._assertSymlink(
self.env.link_cont, link_obj,
expected_content_location='%s/%s' % (
self.env.tgt_cont, normalized_quoted_obj))
def test_symlink_put_head_get(self): def test_symlink_put_head_get(self):
link_obj = uuid4().hex link_obj = uuid4().hex

View File

@ -18,7 +18,7 @@ from copy import deepcopy
import json import json
import time import time
import unittest2 import unittest2
from six.moves.urllib.parse import quote from six.moves.urllib.parse import quote, unquote
import test.functional as tf import test.functional as tf
@ -652,7 +652,7 @@ class TestObjectVersioning(Base):
tgt_b.write("bbbbb") tgt_b.write("bbbbb")
symlink_name = Utils.create_name() symlink_name = Utils.create_name()
sym_tgt_header = '%s/%s' % (container.name, tgt_a_name) sym_tgt_header = quote(unquote('%s/%s' % (container.name, tgt_a_name)))
sym_headers_a = {'X-Symlink-Target': sym_tgt_header} sym_headers_a = {'X-Symlink-Target': sym_tgt_header}
symlink = container.file(symlink_name) symlink = container.file(symlink_name)
symlink.write("", hdrs=sym_headers_a) symlink.write("", hdrs=sym_headers_a)
@ -684,8 +684,9 @@ class TestObjectVersioning(Base):
sym_info = symlink.info(parms={'symlink': 'get'}) sym_info = symlink.info(parms={'symlink': 'get'})
self.assertEqual("aaaaa", symlink.read()) self.assertEqual("aaaaa", symlink.read())
self.assertEqual(MD5_OF_EMPTY_STRING, sym_info['etag']) self.assertEqual(MD5_OF_EMPTY_STRING, sym_info['etag'])
self.assertEqual('%s/%s' % (self.env.container.name, target.name), self.assertEqual(
sym_info['x_symlink_target']) quote(unquote('%s/%s' % (self.env.container.name, target.name))),
sym_info['x_symlink_target'])
def _setup_symlink(self): def _setup_symlink(self):
target = self.env.container.file('target-object') target = self.env.container.file('target-object')

View File

@ -302,9 +302,14 @@ class TestRequest(S3ApiTestCase):
self.assertTrue(sw_req.environ['swift.proxy_access_log_made']) self.assertTrue(sw_req.environ['swift.proxy_access_log_made'])
def test_get_container_info(self): def test_get_container_info(self):
s3api_acl = '{"Owner":"owner","Grant":'\
'[{"Grantee":"owner","Permission":"FULL_CONTROL"}]}'
self.swift.register('HEAD', '/v1/AUTH_test/bucket', HTTPNoContent, self.swift.register('HEAD', '/v1/AUTH_test/bucket', HTTPNoContent,
{'x-container-read': 'foo', {'x-container-read': 'foo',
'X-container-object-count': 5, 'X-container-object-count': 5,
'x-container-sysmeta-versions-location':
'bucket2',
'x-container-sysmeta-s3api-acl': s3api_acl,
'X-container-meta-foo': 'bar'}, None) 'X-container-meta-foo': 'bar'}, None)
req = Request.blank('/bucket', environ={'REQUEST_METHOD': 'GET'}, req = Request.blank('/bucket', environ={'REQUEST_METHOD': 'GET'},
headers={'Authorization': 'AWS test:tester:hmac', headers={'Authorization': 'AWS test:tester:hmac',
@ -316,6 +321,9 @@ class TestRequest(S3ApiTestCase):
self.assertEqual(204, info['status']) # sanity self.assertEqual(204, info['status']) # sanity
self.assertEqual('foo', info['read_acl']) # sanity self.assertEqual('foo', info['read_acl']) # sanity
self.assertEqual('5', info['object_count']) # sanity self.assertEqual('5', info['object_count']) # sanity
self.assertEqual(
'bucket2', info['sysmeta']['versions-location']) # sanity
self.assertEqual(s3api_acl, info['sysmeta']['s3api-acl']) # sanity
self.assertEqual({'foo': 'bar'}, info['meta']) # sanity self.assertEqual({'foo': 'bar'}, info['meta']) # sanity
with patch( with patch(
'swift.common.middleware.s3api.s3request.get_container_info', 'swift.common.middleware.s3api.s3request.get_container_info',

View File

@ -54,6 +54,8 @@ class TestResponse(unittest.TestCase):
expected_headers = HeaderKeyDict( expected_headers = HeaderKeyDict(
{sysmeta_prefix(_server_type) + 'test': 'ok'}) {sysmeta_prefix(_server_type) + 'test': 'ok'})
self.assertEqual(expected_headers, s3resp.sysmeta_headers) self.assertEqual(expected_headers, s3resp.sysmeta_headers)
self.assertIn('x-%s-sysmeta-test-s3api' % _server_type,
s3resp.sw_headers)
def test_response_s3api_sysmeta_from_swift3_sysmeta(self): def test_response_s3api_sysmeta_from_swift3_sysmeta(self):
for _server_type in ('object', 'container'): for _server_type in ('object', 'container'):

View File

@ -372,6 +372,15 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
middleware = s3token.filter_factory(config)(self.app) middleware = s3token.filter_factory(config)(self.app)
self.assertIs('false_ind', middleware._verify) self.assertIs('false_ind', middleware._verify)
def test_reseller_prefix(self):
def do_test(conf, expected):
conf.update(self.conf)
middleware = s3token.filter_factory(conf)(self.app)
self.assertEqual(expected, middleware._reseller_prefix)
do_test({}, 'AUTH_')
do_test({'reseller_prefix': 'KEY_'}, 'KEY_')
do_test({'reseller_prefix': 'KEY'}, 'KEY_')
def test_auth_uris(self): def test_auth_uris(self):
for conf, expected in [ for conf, expected in [
({'auth_uri': 'https://example.com/v2.0'}, ({'auth_uri': 'https://example.com/v2.0'},
@ -456,12 +465,12 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
with self.assertRaises(ConfigFileError) as cm: with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({'auth_uri': auth_uri})(self.app) s3token.filter_factory({'auth_uri': auth_uri})(self.app)
self.assertEqual('Invalid auth_uri; must include scheme and host', self.assertEqual('Invalid auth_uri; must include scheme and host',
cm.exception.message) cm.exception.args[0])
with self.assertRaises(ConfigFileError) as cm: with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({ s3token.filter_factory({
'auth_uri': 'nonhttp://example.com'})(self.app) 'auth_uri': 'nonhttp://example.com'})(self.app)
self.assertEqual('Invalid auth_uri; scheme must be http or https', self.assertEqual('Invalid auth_uri; scheme must be http or https',
cm.exception.message) cm.exception.args[0])
for auth_uri in [ for auth_uri in [
'http://user@example.com/', 'http://user@example.com/',
'http://example.com/?with=query', 'http://example.com/?with=query',
@ -469,7 +478,7 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
with self.assertRaises(ConfigFileError) as cm: with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({'auth_uri': auth_uri})(self.app) s3token.filter_factory({'auth_uri': auth_uri})(self.app)
self.assertEqual('Invalid auth_uri; must not include username, ' self.assertEqual('Invalid auth_uri; must not include username, '
'query, or fragment', cm.exception.message) 'query, or fragment', cm.exception.args[0])
def test_unicode_path(self): def test_unicode_path(self):
url = u'/v1/AUTH_cfa/c/euro\u20ac'.encode('utf8') url = u'/v1/AUTH_cfa/c/euro\u20ac'.encode('utf8')
@ -568,7 +577,7 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
MOCK_REQUEST.return_value = TestResponse({ MOCK_REQUEST.return_value = TestResponse({
'status_code': 201, 'status_code': 201,
'text': json.dumps(GOOD_RESPONSE_V2)}) 'text': json.dumps(GOOD_RESPONSE_V2).encode('ascii')})
req = Request.blank('/v1/AUTH_cfa/c/o') req = Request.blank('/v1/AUTH_cfa/c/o')
req.environ['s3api.auth_details'] = { req.environ['s3api.auth_details'] = {

View File

@ -34,6 +34,8 @@ LIMIT = 'swift.common.constraints.CONTAINER_LISTING_LIMIT'
def md5hex(s): def md5hex(s):
if not isinstance(s, bytes):
s = s.encode('utf-8')
return hashlib.md5(s).hexdigest() return hashlib.md5(s).hexdigest()
@ -52,7 +54,7 @@ class DloTestCase(unittest.TestCase):
headers[0] = h headers[0] = h
body_iter = app(req.environ, start_response) body_iter = app(req.environ, start_response)
body = '' body = b''
# appease the close-checker # appease the close-checker
with closing_if_possible(body_iter): with closing_if_possible(body_iter):
for chunk in body_iter: for chunk in body_iter:
@ -69,36 +71,36 @@ class DloTestCase(unittest.TestCase):
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/seg_01', 'GET', '/v1/AUTH_test/c/seg_01',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("aaaaa")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("aaaaa")},
'aaaaa') b'aaaaa')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/seg_02', 'GET', '/v1/AUTH_test/c/seg_02',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("bbbbb")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("bbbbb")},
'bbbbb') b'bbbbb')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/seg_03', 'GET', '/v1/AUTH_test/c/seg_03',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ccccc")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ccccc")},
'ccccc') b'ccccc')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/seg_04', 'GET', '/v1/AUTH_test/c/seg_04',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ddddd")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ddddd")},
'ddddd') b'ddddd')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/seg_05', 'GET', '/v1/AUTH_test/c/seg_05',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("eeeee")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("eeeee")},
'eeeee') b'eeeee')
# an unrelated object (not seg*) to test the prefix matching # an unrelated object (not seg*) to test the prefix matching
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/catpicture.jpg', 'GET', '/v1/AUTH_test/c/catpicture.jpg',
swob.HTTPOk, {'Content-Length': '9', swob.HTTPOk, {'Content-Length': '9',
'Etag': md5hex("meow meow meow meow")}, 'Etag': md5hex("meow meow meow meow")},
'meow meow meow meow') b'meow meow meow meow')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/mancon/manifest', 'GET', '/v1/AUTH_test/mancon/manifest',
swob.HTTPOk, {'Content-Length': '17', 'Etag': 'manifest-etag', swob.HTTPOk, {'Content-Length': '17', 'Etag': 'manifest-etag',
'X-Object-Manifest': 'c/seg'}, 'X-Object-Manifest': 'c/seg'},
'manifest-contents') b'manifest-contents')
lm = '2013-11-22T02:42:13.781760' lm = '2013-11-22T02:42:13.781760'
ct = 'application/octet-stream' ct = 'application/octet-stream'
@ -120,11 +122,11 @@ class DloTestCase(unittest.TestCase):
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c', 'GET', '/v1/AUTH_test/c',
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
json.dumps(full_container_listing)) json.dumps(full_container_listing).encode('ascii'))
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c?prefix=seg', 'GET', '/v1/AUTH_test/c?prefix=seg',
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
json.dumps(segs)) json.dumps(segs).encode('ascii'))
# This is to let us test multi-page container listings; we use the # This is to let us test multi-page container listings; we use the
# trailing underscore to send small (pagesize=3) listings. # trailing underscore to send small (pagesize=3) listings.
@ -135,26 +137,26 @@ class DloTestCase(unittest.TestCase):
'GET', '/v1/AUTH_test/mancon/manifest-many-segments', 'GET', '/v1/AUTH_test/mancon/manifest-many-segments',
swob.HTTPOk, {'Content-Length': '7', 'Etag': 'etag-manyseg', swob.HTTPOk, {'Content-Length': '7', 'Etag': 'etag-manyseg',
'X-Object-Manifest': 'c/seg_'}, 'X-Object-Manifest': 'c/seg_'},
'manyseg') b'manyseg')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c?prefix=seg_', 'GET', '/v1/AUTH_test/c?prefix=seg_',
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
json.dumps(segs[:3])) json.dumps(segs[:3]).encode('ascii'))
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c?prefix=seg_&marker=seg_03', 'GET', '/v1/AUTH_test/c?prefix=seg_&marker=seg_03',
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
json.dumps(segs[3:])) json.dumps(segs[3:]).encode('ascii'))
# Here's a manifest with 0 segments # Here's a manifest with 0 segments
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/mancon/manifest-no-segments', 'GET', '/v1/AUTH_test/mancon/manifest-no-segments',
swob.HTTPOk, {'Content-Length': '7', 'Etag': 'noseg', swob.HTTPOk, {'Content-Length': '7', 'Etag': 'noseg',
'X-Object-Manifest': 'c/noseg_'}, 'X-Object-Manifest': 'c/noseg_'},
'noseg') b'noseg')
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c?prefix=noseg_', 'GET', '/v1/AUTH_test/c?prefix=noseg_',
swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'},
json.dumps([])) json.dumps([]).encode('ascii'))
class TestDloPutManifest(DloTestCase): class TestDloPutManifest(DloTestCase):
@ -284,7 +286,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(headers["Etag"], expected_etag) self.assertEqual(headers["Etag"], expected_etag)
self.assertEqual(headers["Content-Length"], "25") self.assertEqual(headers["Content-Length"], "25")
self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee')
for _, _, hdrs in self.app.calls_with_headers[1:]: for _, _, hdrs in self.app.calls_with_headers[1:]:
ua = hdrs.get("User-Agent", "") ua = hdrs.get("User-Agent", "")
@ -302,7 +304,7 @@ class TestDloGetManifest(DloTestCase):
req = swob.Request.blank('/v1/AUTH_test/c/catpicture.jpg', req = swob.Request.blank('/v1/AUTH_test/c/catpicture.jpg',
environ={'REQUEST_METHOD': 'GET'}) environ={'REQUEST_METHOD': 'GET'})
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
self.assertEqual(body, "meow meow meow meow") self.assertEqual(body, b"meow meow meow meow")
def test_get_non_object_passthrough(self): def test_get_non_object_passthrough(self):
self.app.register('GET', '/info', swob.HTTPOk, self.app.register('GET', '/info', swob.HTTPOk,
@ -311,7 +313,7 @@ class TestDloGetManifest(DloTestCase):
environ={'REQUEST_METHOD': 'GET'}) environ={'REQUEST_METHOD': 'GET'})
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
self.assertEqual(status, '200 OK') self.assertEqual(status, '200 OK')
self.assertEqual(body, 'useful stuff here') self.assertEqual(body, b'useful stuff here')
self.assertEqual(self.app.call_count, 1) self.assertEqual(self.app.call_count, 1)
def test_get_manifest_passthrough(self): def test_get_manifest_passthrough(self):
@ -328,7 +330,7 @@ class TestDloGetManifest(DloTestCase):
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(headers["Etag"], "manifest-etag") self.assertEqual(headers["Etag"], "manifest-etag")
self.assertEqual(body, "manifest-contents") self.assertEqual(body, b'manifest-contents')
def test_error_passthrough(self): def test_error_passthrough(self):
self.app.register( self.app.register(
@ -347,7 +349,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "10") self.assertEqual(headers["Content-Length"], "10")
self.assertEqual(body, "bbcccccddd") self.assertEqual(body, b'bbcccccddd')
expected_etag = '"%s"' % md5hex( expected_etag = '"%s"' % md5hex(
md5hex("aaaaa") + md5hex("bbbbb") + md5hex("ccccc") + md5hex("aaaaa") + md5hex("bbbbb") + md5hex("ccccc") +
md5hex("ddddd") + md5hex("eeeee")) md5hex("ddddd") + md5hex("eeeee"))
@ -361,7 +363,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "10") self.assertEqual(headers["Content-Length"], "10")
self.assertEqual(body, "cccccddddd") self.assertEqual(body, b'cccccddddd')
def test_get_range_first_byte(self): def test_get_range_first_byte(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -371,7 +373,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "1") self.assertEqual(headers["Content-Length"], "1")
self.assertEqual(body, "a") self.assertEqual(body, b'a')
def test_get_range_last_byte(self): def test_get_range_last_byte(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -381,7 +383,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "1") self.assertEqual(headers["Content-Length"], "1")
self.assertEqual(body, "e") self.assertEqual(body, b'e')
def test_get_range_overlapping_end(self): def test_get_range_overlapping_end(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -392,7 +394,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "7") self.assertEqual(headers["Content-Length"], "7")
self.assertEqual(headers["Content-Range"], "bytes 18-24/25") self.assertEqual(headers["Content-Range"], "bytes 18-24/25")
self.assertEqual(body, "ddeeeee") self.assertEqual(body, b'ddeeeee')
def test_get_range_unsatisfiable(self): def test_get_range_unsatisfiable(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -428,7 +430,7 @@ class TestDloGetManifest(DloTestCase):
# #
# Since the truth is forbidden, we lie. # Since the truth is forbidden, we lie.
self.assertEqual(headers["Content-Range"], "bytes 3-12/15") self.assertEqual(headers["Content-Range"], "bytes 3-12/15")
self.assertEqual(body, "aabbbbbccc") self.assertEqual(body, b"aabbbbbccc")
self.assertEqual( self.assertEqual(
self.app.calls, self.app.calls,
@ -449,7 +451,7 @@ class TestDloGetManifest(DloTestCase):
# this requires multiple pages of container listing, so we can't send # this requires multiple pages of container listing, so we can't send
# a Content-Length header # a Content-Length header
self.assertIsNone(headers.get("Content-Length")) self.assertIsNone(headers.get("Content-Length"))
self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee")
def test_get_suffix_range(self): def test_get_suffix_range(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -459,7 +461,7 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "206 Partial Content") self.assertEqual(status, "206 Partial Content")
self.assertEqual(headers["Content-Length"], "25") self.assertEqual(headers["Content-Length"], "25")
self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee")
def test_get_suffix_range_many_segments(self): def test_get_suffix_range_many_segments(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest-many-segments', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest-many-segments',
@ -471,7 +473,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertIsNone(headers.get("Content-Length")) self.assertIsNone(headers.get("Content-Length"))
self.assertIsNone(headers.get("Content-Range")) self.assertIsNone(headers.get("Content-Range"))
self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee")
def test_get_multi_range(self): def test_get_multi_range(self):
# DLO doesn't support multi-range GETs. The way that you express that # DLO doesn't support multi-range GETs. The way that you express that
@ -485,7 +487,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertIsNone(headers.get("Content-Length")) self.assertIsNone(headers.get("Content-Length"))
self.assertIsNone(headers.get("Content-Range")) self.assertIsNone(headers.get("Content-Range"))
self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee')
def test_if_match_matches(self): def test_if_match_matches(self):
manifest_etag = '"%s"' % md5hex( manifest_etag = '"%s"' % md5hex(
@ -500,7 +502,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '200 OK') self.assertEqual(status, '200 OK')
self.assertEqual(headers['Content-Length'], '25') self.assertEqual(headers['Content-Length'], '25')
self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee')
def test_if_match_does_not_match(self): def test_if_match_does_not_match(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -512,7 +514,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '412 Precondition Failed') self.assertEqual(status, '412 Precondition Failed')
self.assertEqual(headers['Content-Length'], '0') self.assertEqual(headers['Content-Length'], '0')
self.assertEqual(body, '') self.assertEqual(body, b'')
def test_if_none_match_matches(self): def test_if_none_match_matches(self):
manifest_etag = '"%s"' % md5hex( manifest_etag = '"%s"' % md5hex(
@ -527,7 +529,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '304 Not Modified') self.assertEqual(status, '304 Not Modified')
self.assertEqual(headers['Content-Length'], '0') self.assertEqual(headers['Content-Length'], '0')
self.assertEqual(body, '') self.assertEqual(body, b'')
def test_if_none_match_does_not_match(self): def test_if_none_match_does_not_match(self):
req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', req = swob.Request.blank('/v1/AUTH_test/mancon/manifest',
@ -539,7 +541,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '200 OK') self.assertEqual(status, '200 OK')
self.assertEqual(headers['Content-Length'], '25') self.assertEqual(headers['Content-Length'], '25')
self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee')
def test_get_with_if_modified_since(self): def test_get_with_if_modified_since(self):
# It's important not to pass the If-[Un]Modified-Since header to the # It's important not to pass the If-[Un]Modified-Since header to the
@ -581,7 +583,8 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertEqual(''.join(body), "aaaaa") # first segment made it out # first segment made it out
self.assertEqual(body, b'aaaaa')
self.assertEqual(self.dlo.logger.get_lines_for_level('error'), [ self.assertEqual(self.dlo.logger.get_lines_for_level('error'), [
'While processing manifest /v1/AUTH_test/mancon/manifest, ' 'While processing manifest /v1/AUTH_test/mancon/manifest, '
'got 403 while retrieving /v1/AUTH_test/c/seg_02', 'got 403 while retrieving /v1/AUTH_test/c/seg_02',
@ -610,7 +613,7 @@ class TestDloGetManifest(DloTestCase):
with mock.patch(LIMIT, 3): with mock.patch(LIMIT, 3):
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertEqual(body, "aaaaabbbbbccccc") self.assertEqual(body, b'aaaaabbbbbccccc')
def test_error_listing_container_HEAD(self): def test_error_listing_container_HEAD(self):
self.app.register( self.app.register(
@ -639,7 +642,8 @@ class TestDloGetManifest(DloTestCase):
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertEqual(''.join(body), "aaaaabbWRONGbb") # stop after error # stop after error
self.assertEqual(body, b"aaaaabbWRONGbb")
def test_etag_comparison_ignores_quotes(self): def test_etag_comparison_ignores_quotes(self):
# a little future-proofing here in case we ever fix this in swob # a little future-proofing here in case we ever fix this in swob
@ -662,7 +666,7 @@ class TestDloGetManifest(DloTestCase):
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
headers = HeaderKeyDict(headers) headers = HeaderKeyDict(headers)
self.assertEqual(headers["Etag"], self.assertEqual(headers["Etag"],
'"' + hashlib.md5("abcdef").hexdigest() + '"') '"' + hashlib.md5(b"abcdef").hexdigest() + '"')
def test_object_prefix_quoting(self): def test_object_prefix_quoting(self):
self.app.register( self.app.register(
@ -675,22 +679,24 @@ class TestDloGetManifest(DloTestCase):
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c?prefix=%C3%A9', 'GET', '/v1/AUTH_test/c?prefix=%C3%A9',
swob.HTTPOk, {'Content-Type': 'application/json'}, swob.HTTPOk, {'Content-Type': 'application/json'},
json.dumps(segs)) json.dumps(segs).encode('ascii'))
# NB: wsgi string
path = '/v1/AUTH_test/c/\xC3\xa9'
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/\xC3\xa91', 'GET', path + '1',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("AAAAA")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("AAAAA")},
"AAAAA") b"AAAAA")
self.app.register( self.app.register(
'GET', '/v1/AUTH_test/c/\xC3\xA92', 'GET', path + '2',
swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("BBBBB")}, swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("BBBBB")},
"BBBBB") b"BBBBB")
req = swob.Request.blank('/v1/AUTH_test/man/accent', req = swob.Request.blank('/v1/AUTH_test/man/accent',
environ={'REQUEST_METHOD': 'GET'}) environ={'REQUEST_METHOD': 'GET'})
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
self.assertEqual(status, "200 OK") self.assertEqual(status, "200 OK")
self.assertEqual(body, "AAAAABBBBB") self.assertEqual(body, b'AAAAABBBBB')
def test_get_taking_too_long(self): def test_get_taking_too_long(self):
the_time = [time.time()] the_time = [time.time()]
@ -715,7 +721,7 @@ class TestDloGetManifest(DloTestCase):
status, headers, body = self.call_dlo(req) status, headers, body = self.call_dlo(req)
self.assertEqual(status, '200 OK') self.assertEqual(status, '200 OK')
self.assertEqual(body, 'aaaaabbbbbccccc') self.assertEqual(body, b'aaaaabbbbbccccc')
def test_get_oversize_segment(self): def test_get_oversize_segment(self):
# If we send a Content-Length header to the client, it's based on the # If we send a Content-Length header to the client, it's based on the
@ -738,7 +744,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '200 OK') # sanity check self.assertEqual(status, '200 OK') # sanity check
self.assertEqual(headers.get('Content-Length'), '25') # sanity check self.assertEqual(headers.get('Content-Length'), '25') # sanity check
self.assertEqual(body, 'aaaaabbbbbccccccccccccccc') self.assertEqual(body, b'aaaaabbbbbccccccccccccccc')
self.assertEqual( self.assertEqual(
self.app.calls, self.app.calls,
[('GET', '/v1/AUTH_test/mancon/manifest'), [('GET', '/v1/AUTH_test/mancon/manifest'),
@ -770,7 +776,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '200 OK') # sanity check self.assertEqual(status, '200 OK') # sanity check
self.assertEqual(headers.get('Content-Length'), '25') # sanity check self.assertEqual(headers.get('Content-Length'), '25') # sanity check
self.assertEqual(body, 'aaaaabbbbbccccdddddeeeee') self.assertEqual(body, b'aaaaabbbbbccccdddddeeeee')
def test_get_undersize_segment_range(self): def test_get_undersize_segment_range(self):
# Shrink it by a single byte # Shrink it by a single byte
@ -788,7 +794,7 @@ class TestDloGetManifest(DloTestCase):
self.assertEqual(status, '206 Partial Content') # sanity check self.assertEqual(status, '206 Partial Content') # sanity check
self.assertEqual(headers.get('Content-Length'), '15') # sanity check self.assertEqual(headers.get('Content-Length'), '15') # sanity check
self.assertEqual(body, 'aaaaabbbbbcccc') self.assertEqual(body, b'aaaaabbbbbcccc')
def test_get_with_auth_overridden(self): def test_get_with_auth_overridden(self):
auth_got_called = [0] auth_got_called = [0]
@ -840,7 +846,7 @@ class TestDloConfiguration(unittest.TestCase):
max_get_time = 2900 max_get_time = 2900
""") """)
conffile = tempfile.NamedTemporaryFile() conffile = tempfile.NamedTemporaryFile(mode='w')
conffile.write(proxy_conf) conffile.write(proxy_conf)
conffile.flush() conffile.flush()
@ -853,6 +859,8 @@ class TestDloConfiguration(unittest.TestCase):
self.assertEqual(10, mware.rate_limit_after_segment) self.assertEqual(10, mware.rate_limit_after_segment)
self.assertEqual(3600, mware.max_get_time) self.assertEqual(3600, mware.max_get_time)
conffile.close()
def test_finding_defaults_from_file(self): def test_finding_defaults_from_file(self):
# If DLO has no config vars, go pull them from the proxy server's # If DLO has no config vars, go pull them from the proxy server's
# config section # config section
@ -875,7 +883,7 @@ class TestDloConfiguration(unittest.TestCase):
set max_get_time = 2900 set max_get_time = 2900
""") """)
conffile = tempfile.NamedTemporaryFile() conffile = tempfile.NamedTemporaryFile(mode='w')
conffile.write(proxy_conf) conffile.write(proxy_conf)
conffile.flush() conffile.flush()
@ -887,6 +895,8 @@ class TestDloConfiguration(unittest.TestCase):
self.assertEqual(13, mware.rate_limit_after_segment) self.assertEqual(13, mware.rate_limit_after_segment)
self.assertEqual(2900, mware.max_get_time) self.assertEqual(2900, mware.max_get_time)
conffile.close()
def test_finding_defaults_from_dir(self): def test_finding_defaults_from_dir(self):
# If DLO has no config vars, go pull them from the proxy server's # If DLO has no config vars, go pull them from the proxy server's
# config section # config section
@ -913,11 +923,13 @@ class TestDloConfiguration(unittest.TestCase):
conf_dir = self.tmpdir conf_dir = self.tmpdir
conffile1 = tempfile.NamedTemporaryFile(dir=conf_dir, suffix='.conf') conffile1 = tempfile.NamedTemporaryFile(mode='w',
dir=conf_dir, suffix='.conf')
conffile1.write(proxy_conf1) conffile1.write(proxy_conf1)
conffile1.flush() conffile1.flush()
conffile2 = tempfile.NamedTemporaryFile(dir=conf_dir, suffix='.conf') conffile2 = tempfile.NamedTemporaryFile(mode='w',
dir=conf_dir, suffix='.conf')
conffile2.write(proxy_conf2) conffile2.write(proxy_conf2)
conffile2.flush() conffile2.flush()
@ -929,6 +941,9 @@ class TestDloConfiguration(unittest.TestCase):
self.assertEqual(13, mware.rate_limit_after_segment) self.assertEqual(13, mware.rate_limit_after_segment)
self.assertEqual(2900, mware.max_get_time) self.assertEqual(2900, mware.max_get_time)
conffile1.close()
conffile2.close()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -528,7 +528,7 @@ class TestStaticWeb(unittest.TestCase):
def test_container3indexhtml(self): def test_container3indexhtml(self):
resp = Request.blank('/v1/a/c3/').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c3/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Test main index.html file.' in resp.body) self.assertIn(b'Test main index.html file.', resp.body)
def test_container3subsubdir(self): def test_container3subsubdir(self):
resp = Request.blank( resp = Request.blank(
@ -539,16 +539,16 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank( resp = Request.blank(
'/v1/a/c3/subdir3/subsubdir/').get_response(self.test_staticweb) '/v1/a/c3/subdir3/subsubdir/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertEqual(resp.body, 'index file') self.assertEqual(resp.body, b'index file')
def test_container3subdir(self): def test_container3subdir(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c3/subdir/').get_response(self.test_staticweb) '/v1/a/c3/subdir/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertIn('Listing of /v1/a/c3/subdir/', resp.body) self.assertIn(b'Listing of /v1/a/c3/subdir/', resp.body)
self.assertIn('</style>', resp.body) self.assertIn(b'</style>', resp.body)
self.assertNotIn('<link', resp.body) self.assertNotIn(b'<link', resp.body)
self.assertNotIn('listing.css', resp.body) self.assertNotIn(b'listing.css', resp.body)
def test_container3subdirx(self): def test_container3subdirx(self):
resp = Request.blank( resp = Request.blank(
@ -569,18 +569,18 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank( resp = Request.blank(
'/v1/a/c3/unknown').get_response(self.test_staticweb) '/v1/a/c3/unknown').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertNotIn("Chrome's 404 fancy-page sucks.", resp.body) self.assertNotIn(b"Chrome's 404 fancy-page sucks.", resp.body)
def test_container3bindexhtml(self): def test_container3bindexhtml(self):
resp = Request.blank('/v1/a/c3b/').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c3b/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 204) self.assertEqual(resp.status_int, 204)
self.assertEqual(resp.body, '') self.assertEqual(resp.body, b'')
def test_container4indexhtml(self): def test_container4indexhtml(self):
resp = Request.blank('/v1/a/c4/').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c4/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertIn('Listing of /v1/a/c4/', resp.body) self.assertIn(b'Listing of /v1/a/c4/', resp.body)
self.assertIn('href="listing.css"', resp.body) self.assertIn(b'href="listing.css"', resp.body)
def test_container4indexhtmlauthed(self): def test_container4indexhtmlauthed(self):
resp = Request.blank('/v1/a/c4').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c4').get_response(self.test_staticweb)
@ -600,16 +600,16 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank( resp = Request.blank(
'/v1/a/c4/unknown').get_response(self.test_staticweb) '/v1/a/c4/unknown').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertIn("Chrome's 404 fancy-page sucks.", resp.body) self.assertIn(b"Chrome's 404 fancy-page sucks.", resp.body)
def test_container4subdir(self): def test_container4subdir(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c4/subdir/').get_response(self.test_staticweb) '/v1/a/c4/subdir/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertIn('Listing of /v1/a/c4/subdir/', resp.body) self.assertIn(b'Listing of /v1/a/c4/subdir/', resp.body)
self.assertNotIn('</style>', resp.body) self.assertNotIn(b'</style>', resp.body)
self.assertIn('<link', resp.body) self.assertIn(b'<link', resp.body)
self.assertIn('href="../listing.css"', resp.body) self.assertIn(b'href="../listing.css"', resp.body)
self.assertEqual(resp.headers['content-type'], self.assertEqual(resp.headers['content-type'],
'text/html; charset=UTF-8') 'text/html; charset=UTF-8')
@ -631,7 +631,7 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank( resp = Request.blank(
'/v1/a/c5/unknown').get_response(self.test_staticweb) '/v1/a/c5/unknown').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertNotIn("Chrome's 404 fancy-page sucks.", resp.body) self.assertNotIn(b"Chrome's 404 fancy-page sucks.", resp.body)
def test_container6subdir(self): def test_container6subdir(self):
resp = Request.blank( resp = Request.blank(
@ -649,7 +649,7 @@ class TestStaticWeb(unittest.TestCase):
staticweb.filter_factory({})(self.app), deny_listing=True) staticweb.filter_factory({})(self.app), deny_listing=True)
resp = Request.blank('/v1/a/c6/').get_response(test_staticweb) resp = Request.blank('/v1/a/c6/').get_response(test_staticweb)
self.assertEqual(resp.status_int, 401) self.assertEqual(resp.status_int, 401)
self.assertIn("Hey, you're not authorized to see this!", resp.body) self.assertIn(b"Hey, you're not authorized to see this!", resp.body)
# expect default 401 if request is not auth'd for listing or object GET # expect default 401 if request is not auth'd for listing or object GET
test_staticweb = FakeAuthFilter( test_staticweb = FakeAuthFilter(
@ -657,20 +657,20 @@ class TestStaticWeb(unittest.TestCase):
deny_objects=True) deny_objects=True)
resp = Request.blank('/v1/a/c6/').get_response(test_staticweb) resp = Request.blank('/v1/a/c6/').get_response(test_staticweb)
self.assertEqual(resp.status_int, 401) self.assertEqual(resp.status_int, 401)
self.assertNotIn("Hey, you're not authorized to see this!", resp.body) self.assertNotIn(b"Hey, you're not authorized to see this!", resp.body)
def test_container6blisting(self): def test_container6blisting(self):
label = 'Listing of {0}/'.format( label = 'Listing of {0}/'.format(
meta_map['c6b']['meta']['web-listings-label']) meta_map['c6b']['meta']['web-listings-label'])
resp = Request.blank('/v1/a/c6b/').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c6b/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertIn(label, resp.body) self.assertIn(label.encode('utf-8'), resp.body)
def test_container7listing(self): def test_container7listing(self):
# container7 has web-listings = f, web-error=error.html # container7 has web-listings = f, web-error=error.html
resp = Request.blank('/v1/a/c7/').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c7/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertIn("Web Listing Disabled", resp.body) self.assertIn(b"Web Listing Disabled", resp.body)
# expect 301 if auth'd but no trailing '/' # expect 301 if auth'd but no trailing '/'
resp = Request.blank('/v1/a/c7').get_response(self.test_staticweb) resp = Request.blank('/v1/a/c7').get_response(self.test_staticweb)
@ -682,14 +682,14 @@ class TestStaticWeb(unittest.TestCase):
deny_objects=True) deny_objects=True)
resp = Request.blank('/v1/a/c7').get_response(test_staticweb) resp = Request.blank('/v1/a/c7').get_response(test_staticweb)
self.assertEqual(resp.status_int, 401) self.assertEqual(resp.status_int, 401)
self.assertNotIn("Hey, you're not authorized to see this!", resp.body) self.assertNotIn(b"Hey, you're not authorized to see this!", resp.body)
# expect custom 401 if request is not auth'd for listing # expect custom 401 if request is not auth'd for listing
test_staticweb = FakeAuthFilter( test_staticweb = FakeAuthFilter(
staticweb.filter_factory({})(self.app), deny_listing=True) staticweb.filter_factory({})(self.app), deny_listing=True)
resp = Request.blank('/v1/a/c7/').get_response(test_staticweb) resp = Request.blank('/v1/a/c7/').get_response(test_staticweb)
self.assertEqual(resp.status_int, 401) self.assertEqual(resp.status_int, 401)
self.assertIn("Hey, you're not authorized to see this!", resp.body) self.assertIn(b"Hey, you're not authorized to see this!", resp.body)
# expect default 401 if request is not auth'd for listing or object GET # expect default 401 if request is not auth'd for listing or object GET
test_staticweb = FakeAuthFilter( test_staticweb = FakeAuthFilter(
@ -697,69 +697,69 @@ class TestStaticWeb(unittest.TestCase):
deny_objects=True) deny_objects=True)
resp = Request.blank('/v1/a/c7/').get_response(test_staticweb) resp = Request.blank('/v1/a/c7/').get_response(test_staticweb)
self.assertEqual(resp.status_int, 401) self.assertEqual(resp.status_int, 401)
self.assertNotIn("Hey, you're not authorized to see this!", resp.body) self.assertNotIn(b"Hey, you're not authorized to see this!", resp.body)
def test_container8listingcss(self): def test_container8listingcss(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c8/').get_response(self.test_staticweb) '/v1/a/c8/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c8/' in resp.body) self.assertIn(b'Listing of /v1/a/c8/', resp.body)
self.assertTrue('<link' in resp.body) self.assertIn(b'<link', resp.body)
self.assertTrue( self.assertIn(b'href="http://localhost/stylesheets/listing.css"',
'href="http://localhost/stylesheets/listing.css"' in resp.body) resp.body)
def test_container8subdirlistingcss(self): def test_container8subdirlistingcss(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c8/subdir/').get_response(self.test_staticweb) '/v1/a/c8/subdir/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c8/subdir/' in resp.body) self.assertIn(b'Listing of /v1/a/c8/subdir/', resp.body)
self.assertTrue('<link' in resp.body) self.assertIn(b'<link', resp.body)
self.assertTrue( self.assertIn(b'href="http://localhost/stylesheets/listing.css"',
'href="http://localhost/stylesheets/listing.css"' in resp.body) resp.body)
def test_container9listingcss(self): def test_container9listingcss(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c9/').get_response(self.test_staticweb) '/v1/a/c9/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c9/' in resp.body) self.assertIn(b'Listing of /v1/a/c9/', resp.body)
self.assertTrue('<link' in resp.body) self.assertIn(b'<link', resp.body)
self.assertTrue('href="/absolute/listing.css"' in resp.body) self.assertIn(b'href="/absolute/listing.css"', resp.body)
def test_container9subdirlistingcss(self): def test_container9subdirlistingcss(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c9/subdir/').get_response(self.test_staticweb) '/v1/a/c9/subdir/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c9/subdir/' in resp.body) self.assertIn(b'Listing of /v1/a/c9/subdir/', resp.body)
self.assertTrue('<link' in resp.body) self.assertIn(b'<link', resp.body)
self.assertTrue('href="/absolute/listing.css"' in resp.body) self.assertIn(b'href="/absolute/listing.css"', resp.body)
def test_container10unicodesubdirlisting(self): def test_container10unicodesubdirlisting(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c10/').get_response(self.test_staticweb) '/v1/a/c10/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c10/' in resp.body) self.assertIn(b'Listing of /v1/a/c10/', resp.body)
resp = Request.blank( resp = Request.blank(
'/v1/a/c10/\xe2\x98\x83/').get_response(self.test_staticweb) '/v1/a/c10/\xe2\x98\x83/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('Listing of /v1/a/c10/\xe2\x98\x83/' in resp.body) self.assertIn(b'Listing of /v1/a/c10/\xe2\x98\x83/', resp.body)
resp = Request.blank( resp = Request.blank(
'/v1/a/c10/\xe2\x98\x83/\xe2\x98\x83/' '/v1/a/c10/\xe2\x98\x83/\xe2\x98\x83/'
).get_response(self.test_staticweb) ).get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue( self.assertIn(
'Listing of /v1/a/c10/\xe2\x98\x83/\xe2\x98\x83/' in resp.body) b'Listing of /v1/a/c10/\xe2\x98\x83/\xe2\x98\x83/', resp.body)
def test_container11subdirmarkerobjectindex(self): def test_container11subdirmarkerobjectindex(self):
resp = Request.blank('/v1/a/c11/subdir/').get_response( resp = Request.blank('/v1/a/c11/subdir/').get_response(
self.test_staticweb) self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertTrue('<h2>c11 subdir index</h2>' in resp.body) self.assertIn(b'<h2>c11 subdir index</h2>', resp.body)
def test_container11subdirmarkermatchdirtype(self): def test_container11subdirmarkermatchdirtype(self):
resp = Request.blank('/v1/a/c11a/subdir/').get_response( resp = Request.blank('/v1/a/c11a/subdir/').get_response(
self.test_staticweb) self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertIn('Index File Not Found', resp.body) self.assertIn(b'Index File Not Found', resp.body)
def test_container11subdirmarkeraltdirtype(self): def test_container11subdirmarkeraltdirtype(self):
resp = Request.blank('/v1/a/c11a/subdir2/').get_response( resp = Request.blank('/v1/a/c11a/subdir2/').get_response(
@ -775,27 +775,27 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank('/v1/a/c12/').get_response( resp = Request.blank('/v1/a/c12/').get_response(
self.test_staticweb) self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertIn('index file', resp.body) self.assertIn(b'index file', resp.body)
def test_container_404_has_css(self): def test_container_404_has_css(self):
resp = Request.blank('/v1/a/c13/').get_response( resp = Request.blank('/v1/a/c13/').get_response(
self.test_staticweb) self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertIn('listing.css', resp.body) self.assertIn(b'listing.css', resp.body)
def test_container_404_has_no_css(self): def test_container_404_has_no_css(self):
resp = Request.blank('/v1/a/c7/').get_response( resp = Request.blank('/v1/a/c7/').get_response(
self.test_staticweb) self.test_staticweb)
self.assertEqual(resp.status_int, 404) self.assertEqual(resp.status_int, 404)
self.assertNotIn('listing.css', resp.body) self.assertNotIn(b'listing.css', resp.body)
self.assertIn('<style', resp.body) self.assertIn(b'<style', resp.body)
def test_subrequest_once_if_possible(self): def test_subrequest_once_if_possible(self):
resp = Request.blank( resp = Request.blank(
'/v1/a/c4/one.txt').get_response(self.test_staticweb) '/v1/a/c4/one.txt').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertEqual(resp.headers['x-object-meta-test'], 'value') self.assertEqual(resp.headers['x-object-meta-test'], 'value')
self.assertEqual(resp.body, '1') self.assertEqual(resp.body, b'1')
self.assertEqual(self.app.calls, 1) self.assertEqual(self.app.calls, 1)
def test_no_auth_middleware(self): def test_no_auth_middleware(self):

View File

@ -2463,28 +2463,6 @@ log_name = %(yarr)s'''
self.verify_under_pseudo_time(testfunc, target_runtime_ms=900) self.verify_under_pseudo_time(testfunc, target_runtime_ms=900)
def test_urlparse(self):
parsed = utils.urlparse('http://127.0.0.1/')
self.assertEqual(parsed.scheme, 'http')
self.assertEqual(parsed.hostname, '127.0.0.1')
self.assertEqual(parsed.path, '/')
parsed = utils.urlparse('http://127.0.0.1:8080/')
self.assertEqual(parsed.port, 8080)
parsed = utils.urlparse('https://127.0.0.1/')
self.assertEqual(parsed.scheme, 'https')
parsed = utils.urlparse('http://[::1]/')
self.assertEqual(parsed.hostname, '::1')
parsed = utils.urlparse('http://[::1]:8080/')
self.assertEqual(parsed.hostname, '::1')
self.assertEqual(parsed.port, 8080)
parsed = utils.urlparse('www.example.com')
self.assertEqual(parsed.hostname, '')
def test_search_tree(self): def test_search_tree(self):
# file match & ext miss # file match & ext miss
with temptree(['asdf.conf', 'blarg.conf', 'asdf.cfg']) as t: with temptree(['asdf.conf', 'blarg.conf', 'asdf.cfg']) as t:

View File

@ -1171,7 +1171,7 @@ class TestReconciler(unittest.TestCase):
q_path = '.misplaced_objects/%s' % container q_path = '.misplaced_objects/%s' % container
self._mock_listing({ self._mock_listing({
(None, "/%s/1:/AUTH_bob/c/o1" % q_path): q_ts, (None, "/%s/1:/AUTH_bob/c/o1" % q_path): q_ts,
(1, '/AUTH_bob/c/o1'): q_ts - 0.00001, # slightly older (1, '/AUTH_bob/c/o1'): q_ts - 1, # slightly older
}) })
self._mock_oldest_spi({'c': 0}) self._mock_oldest_spi({'c': 0})
deleted_container_entries = self._run_once() deleted_container_entries = self._run_once()
@ -1411,7 +1411,7 @@ class TestReconciler(unittest.TestCase):
self._mock_listing({ self._mock_listing({
(None, "/.misplaced_objects/3600/1:/AUTH_bob/c/o1"): 3679.2019, (None, "/.misplaced_objects/3600/1:/AUTH_bob/c/o1"): 3679.2019,
(1, "/AUTH_bob/c/o1"): 3679.2019, (1, "/AUTH_bob/c/o1"): 3679.2019,
(0, "/AUTH_bob/c/o1"): 3679.2019 + 0.00001, # slightly newer (0, "/AUTH_bob/c/o1"): 3679.2019 + 1, # slightly newer
}) })
self._mock_oldest_spi({'c': 0}) self._mock_oldest_spi({'c': 0})
deleted_container_entries = self._run_once() deleted_container_entries = self._run_once()
@ -1452,7 +1452,7 @@ class TestReconciler(unittest.TestCase):
self._mock_listing({ self._mock_listing({
(None, "/.misplaced_objects/36000/1:/AUTH_bob/c/o1"): 36123.38393, (None, "/.misplaced_objects/36000/1:/AUTH_bob/c/o1"): 36123.38393,
(1, "/AUTH_bob/c/o1"): 36123.38393, (1, "/AUTH_bob/c/o1"): 36123.38393,
(0, "/AUTH_bob/c/o1"): 36123.38393 - 0.00001, # slightly older (0, "/AUTH_bob/c/o1"): 36123.38393 - 1, # slightly older
}) })
self._mock_oldest_spi({'c': 0}) self._mock_oldest_spi({'c': 0})
deleted_container_entries = self._run_once() deleted_container_entries = self._run_once()

View File

@ -678,7 +678,7 @@ class TestAuditor(unittest.TestCase):
auditor_worker.audit_all_objects(device_dirs=['sda']) auditor_worker.audit_all_objects(device_dirs=['sda'])
log_lines = self.logger.get_lines_for_level('info') log_lines = self.logger.get_lines_for_level('info')
self.assertGreater(len(log_lines), 0) self.assertGreater(len(log_lines), 0)
self.assertTrue(log_lines[0].index('ALL - parallel, sda')) self.assertIn('ALL - parallel, sda', log_lines[0])
self.logger.clear() self.logger.clear()
auditor_worker = auditor.AuditorWorker(self.conf, self.logger, auditor_worker = auditor.AuditorWorker(self.conf, self.logger,
@ -687,7 +687,7 @@ class TestAuditor(unittest.TestCase):
auditor_worker.audit_all_objects(device_dirs=['sda']) auditor_worker.audit_all_objects(device_dirs=['sda'])
log_lines = self.logger.get_lines_for_level('info') log_lines = self.logger.get_lines_for_level('info')
self.assertGreater(len(log_lines), 0) self.assertGreater(len(log_lines), 0)
self.assertTrue(log_lines[0].index('ZBF - sda')) self.assertIn('ZBF - sda', log_lines[0])
def test_object_run_recon_cache(self): def test_object_run_recon_cache(self):
ts = Timestamp(time.time()) ts = Timestamp(time.time())

View File

@ -536,9 +536,27 @@ class TestObjectReplicator(unittest.TestCase):
self._write_disk_data('sdd', with_json=True) self._write_disk_data('sdd', with_json=True)
_create_test_rings(self.testdir, devs) _create_test_rings(self.testdir, devs)
self.replicator.collect_jobs() self.replicator.collect_jobs(override_partitions=[1])
self.assertEqual(self.replicator.total_stats.failure, 0) self.assertEqual(self.replicator.total_stats.failure, 0)
def test_collect_jobs_with_override_parts_and_unexpected_part_dir(self):
self.replicator.collect_jobs(override_partitions=[0, 2])
self.assertEqual(self.replicator.total_stats.failure, 0)
os.mkdir(os.path.join(self.objects_1, 'foo'))
jobs = self.replicator.collect_jobs(override_partitions=[0, 2])
found_jobs = set()
for j in jobs:
found_jobs.add((int(j['policy']), int(j['partition'])))
self.assertEqual(found_jobs, {
(0, 0),
(0, 2),
(1, 0),
(1, 2),
})
num_disks = len(POLICIES[1].object_ring.devs)
# N.B. it's not clear why the UUT increments failure per device
self.assertEqual(self.replicator.total_stats.failure, num_disks)
@mock.patch('swift.obj.replicator.random.shuffle', side_effect=lambda l: l) @mock.patch('swift.obj.replicator.random.shuffle', side_effect=lambda l: l)
def test_collect_jobs_multi_disk(self, mock_shuffle): def test_collect_jobs_multi_disk(self, mock_shuffle):
devs = [ devs = [
@ -1401,10 +1419,10 @@ class TestObjectReplicator(unittest.TestCase):
self.assertTrue(os.access(part_path, os.F_OK)) self.assertTrue(os.access(part_path, os.F_OK))
self.replicator.replicate(override_devices=['sdb']) self.replicator.replicate(override_devices=['sdb'])
self.assertTrue(os.access(part_path, os.F_OK)) self.assertTrue(os.access(part_path, os.F_OK))
self.replicator.replicate(override_partitions=['9']) self.replicator.replicate(override_partitions=[9])
self.assertTrue(os.access(part_path, os.F_OK)) self.assertTrue(os.access(part_path, os.F_OK))
self.replicator.replicate(override_devices=['sda'], self.replicator.replicate(override_devices=['sda'],
override_partitions=['1']) override_partitions=[1])
self.assertFalse(os.access(part_path, os.F_OK)) self.assertFalse(os.access(part_path, os.F_OK))
def test_delete_policy_override_params(self): def test_delete_policy_override_params(self):

View File

@ -25,6 +25,6 @@ function is_rhel7 {
if is_rhel7; then if is_rhel7; then
# Install CentOS OpenStack repos so that we have access to some extra # Install CentOS OpenStack repos so that we have access to some extra
# packages. # packages.
sudo yum install -y centos-release-openstack-queens sudo yum install -y centos-release-openstack-rocky
sudo yum install -y liberasurecode-devel sudo yum install -y liberasurecode-devel
fi fi

37
tox.ini
View File

@ -1,18 +1,19 @@
[tox] [tox]
envlist = py35,py27,pep8 envlist = py37,py27,pep8
minversion = 2.3.2 minversion = 2.3.2
skipsdist = True skipsdist = True
[testenv] [testenv]
usedevelop = True usedevelop = True
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -U {opts} {packages} install_command = pip install -U {opts} {packages}
setenv = VIRTUAL_ENV={envdir} setenv = VIRTUAL_ENV={envdir}
NOSE_WITH_COVERAGE=1 NOSE_WITH_COVERAGE=1
NOSE_COVER_BRANCHES=1 NOSE_COVER_BRANCHES=1
deps = deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt}
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
commands = find . -type f -name "*.py[c|o]" -delete commands = find . ( -type f -o -type l ) -name "*.py[c|o]" -delete
find . -type d -name "__pycache__" -delete find . -type d -name "__pycache__" -delete
nosetests {posargs:test/unit} nosetests {posargs:test/unit}
whitelist_externals = find whitelist_externals = find
@ -31,12 +32,15 @@ setenv = VIRTUAL_ENV={envdir}
# tests file by file, add them into the list below, and never go back. # tests file by file, add them into the list below, and never go back.
# This list also serves as a shared task board for those helping with py3. # This list also serves as a shared task board for those helping with py3.
# But mind that reviews expanding this list may be outstanding in Gerrit. # But mind that reviews expanding this list may be outstanding in Gerrit.
[testenv:py35] [testenv:py37]
commands = commands =
find . ( -type f -o -type l ) -name "*.py[c|o]" -delete
find . -type d -name "__pycache__" -delete
nosetests {posargs:\ nosetests {posargs:\
test/unit/account \ test/unit/account \
test/unit/cli \ test/unit/cli \
test/unit/common/middleware/crypto \ test/unit/common/middleware/crypto \
test/unit/common/middleware/s3api/test_s3token.py \
test/unit/common/middleware/test_account_quotas.py \ test/unit/common/middleware/test_account_quotas.py \
test/unit/common/middleware/test_acl.py \ test/unit/common/middleware/test_acl.py \
test/unit/common/middleware/test_catch_errors.py \ test/unit/common/middleware/test_catch_errors.py \
@ -44,6 +48,7 @@ commands =
test/unit/common/middleware/test_container_sync.py \ test/unit/common/middleware/test_container_sync.py \
test/unit/common/middleware/test_copy.py \ test/unit/common/middleware/test_copy.py \
test/unit/common/middleware/test_crossdomain.py \ test/unit/common/middleware/test_crossdomain.py \
test/unit/common/middleware/test_dlo.py \
test/unit/common/middleware/test_domain_remap.py \ test/unit/common/middleware/test_domain_remap.py \
test/unit/common/middleware/test_formpost.py \ test/unit/common/middleware/test_formpost.py \
test/unit/common/middleware/test_gatekeeper.py \ test/unit/common/middleware/test_gatekeeper.py \
@ -59,6 +64,7 @@ commands =
test/unit/common/middleware/test_read_only.py \ test/unit/common/middleware/test_read_only.py \
test/unit/common/middleware/test_recon.py \ test/unit/common/middleware/test_recon.py \
test/unit/common/middleware/test_subrequest_logging.py \ test/unit/common/middleware/test_subrequest_logging.py \
test/unit/common/middleware/test_staticweb.py \
test/unit/common/middleware/test_tempauth.py \ test/unit/common/middleware/test_tempauth.py \
test/unit/common/middleware/test_versioned_writes.py \ test/unit/common/middleware/test_versioned_writes.py \
test/unit/common/middleware/test_xprofile.py \ test/unit/common/middleware/test_xprofile.py \
@ -95,31 +101,23 @@ commands =
test/unit/proxy/controllers/test_obj.py} test/unit/proxy/controllers/test_obj.py}
[testenv:py36] [testenv:py36]
commands = {[testenv:py35]commands} commands = {[testenv:py37]commands}
[testenv:py37] [testenv:py35]
commands = {[testenv:py35]commands} commands = {[testenv:py37]commands}
[testenv:pep8] [testenv:pep8]
basepython = python2.7 basepython = python2.7
commands = commands =
flake8 {posargs:swift test doc setup.py} flake8 {posargs:swift test doc setup.py}
flake8 --filename=swift* bin flake8 --filename=swift* bin
python ./setup.py check --restructuredtext --strict
bandit -c bandit.yaml -r swift -n 5 bandit -c bandit.yaml -r swift -n 5
./.manpages {posargs} ./.manpages {posargs}
[testenv:py3pep8] [testenv:py3pep8]
basepython = python3 basepython = python3
install_command = echo {packages} commands = {[testenv:pep8]commands}
whitelist_externals = echo
commands =
# Gross hack. There's no other way to get it to /not/ install swift itself
# (which triggers installing eventlet) but also get flake8 installed.
pip install flake8
flake8 swift test doc setup.py
flake8 --filename=swift* bin
python ./setup.py check --restructuredtext --strict
./.manpages {posargs}
[testenv:func] [testenv:func]
basepython = python2.7 basepython = python2.7
@ -149,14 +147,14 @@ setenv = SWIFT_TEST_IN_PROCESS=1
commands = {posargs} commands = {posargs}
[testenv:docs] [testenv:docs]
basepython = python2.7 basepython = python3
deps = -r{toxinidir}/doc/requirements.txt deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -W -b html doc/source doc/build/html commands = sphinx-build -W -b html doc/source doc/build/html
[testenv:api-ref] [testenv:api-ref]
# This environment is called from CI scripts to test and publish # This environment is called from CI scripts to test and publish
# the API Ref to developer.openstack.org. # the API Ref to developer.openstack.org.
basepython = python2.7 basepython = python3
deps = -r{toxinidir}/doc/requirements.txt deps = -r{toxinidir}/doc/requirements.txt
commands = commands =
rm -rf api-ref/build rm -rf api-ref/build
@ -191,6 +189,7 @@ deps = bindep
commands = bindep test commands = bindep test
[testenv:releasenotes] [testenv:releasenotes]
basepython = python3
deps = -r{toxinidir}/doc/requirements.txt deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html