Merge remote-tracking branch 'remotes/origin/master' into feature/deep

Change-Id: I8cbc788476385645e0333ce481a76a666f02bb25
This commit is contained in:
Alistair Coles 2018-01-02 13:09:53 +00:00
commit e2f7804924
64 changed files with 4687 additions and 208 deletions

137
.zuul.yaml Normal file
View File

@ -0,0 +1,137 @@
- job:
name: swift-tox-base
parent: openstack-tox-py27
description: |
Base job for swift-tox jobs.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
timeout: 2400
vars:
tox_environment:
TMPDIR: "{{ ansible_env.HOME }}/xfstmp"
- job:
name: swift-tox-py27
parent: swift-tox-base
description: |
Run unit-tests for swift under cPython version 2.7.
Uses tox with the ``py27`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
vars:
tox_envlist: py27
- job:
name: swift-tox-py27-centos-7
parent: swift-tox-py27
nodeset: centos-7
- job:
name: swift-tox-py35
parent: swift-tox-base
description: |
Run unit-tests for swift under cPython version 3.5.
Uses tox with the ``py35`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
vars:
tox_envlist: py35
bindep_profile: test py35
- job:
name: swift-tox-func
parent: swift-tox-base
description: |
Run functional tests for swift under cPython version 2.7.
Uses tox with the ``func`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
vars:
tox_envlist: func
- job:
name: swift-tox-func-centos-7
parent: swift-tox-func
nodeset: centos-7
- job:
name: swift-tox-func-post-as-copy
parent: swift-tox-base
description: |
Run functional tests for swift under cPython version 2.7.
Uses tox with the ``func-post-as-copy`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
vars:
tox_envlist: func-post-as-copy
- job:
name: swift-tox-func-post-as-copy-centos-7
parent: swift-tox-func-post-as-copy
nodeset: centos-7
- job:
name: swift-tox-func-encryption
parent: swift-tox-base
description: |
Run functional tests for swift under cPython version 2.7.
Uses tox with the ``func-encryption`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
vars:
tox_envlist: func-encryption
- job:
name: swift-tox-func-encryption-centos-7
parent: swift-tox-func-encryption
nodeset: centos-7
- job:
name: swift-tox-func-ec
parent: swift-tox-base
description: |
Run functional tests for swift under cPython version 2.7.
Uses tox with the ``func-post-ec`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
branches: ^(?!stable/ocata).*$
vars:
tox_envlist: func-ec
- job:
name: swift-tox-func-ec-centos-7
parent: swift-tox-func-ec
nodeset: centos-7
- project:
name: openstack/swift
check:
jobs:
- swift-tox-py27
- swift-tox-py35
- swift-tox-func
- swift-tox-func-post-as-copy
- swift-tox-func-encryption
- swift-tox-func-ec
gate:
jobs:
- swift-tox-py27
- swift-tox-py35
- swift-tox-func
- swift-tox-func-post-as-copy
- swift-tox-func-encryption
- swift-tox-func-ec
experimental:
jobs:
- swift-tox-py27-centos-7
- swift-tox-func-centos-7
- swift-tox-func-post-as-copy-centos-7
- swift-tox-func-encryption-centos-7
- swift-tox-func-ec-centos-7

View File

@ -30,6 +30,7 @@ David Goetz (david.goetz@rackspace.com)
Greg Lange (greglange@gmail.com)
Janie Richling (jrichli@us.ibm.com)
Michael Barton (mike@weirdlooking.com)
Mahati Chamarthy (mahati.chamarthy@gmail.com)
Contributors
------------

View File

@ -164,7 +164,7 @@ ETag_obj_req:
manifest objects, this value is the MD5 checksum of the
concatenated string of ETag values for each of the segments in
the manifest. You are strongly recommended to compute
the MD5 checksum value and include it in the request. This
the MD5 checksum value and include it in the request. This
enables the Object Storage API to check the integrity of the
upload. The value is not quoted.
in: header
@ -850,6 +850,44 @@ X-Storage-Policy:
in: header
required: false
type: string
X-Symlink-Target:
description: |
Set to specify that this is a symlink object.
The value is the relative path of the target object in the
format <container>/<object>. The target object does not need to
exist at the time of symlink creation.
You must UTF-8-encode and then URL-encode the names of the
container and object before you include them in this header.
in: header
required: false
type: string
X-Symlink-Target-Account:
description: |
Set to specify that this is a cross-account symlink to
an object in the account specified in the value.
The ``X-Symlink-Target`` must also be set for this to
be effective.
You must UTF-8-encode and then URL-encode the account name
before you include it in this header.
in: header
required: false
type: string
X-Symlink-Target-Account_resp:
description: |
If present, and ``X-Symlink-Target`` is present, then
this is a cross-account symlink to
an object in the account specified in the value.
in: header
required: false
type: string
X-Symlink-Target_resp:
description: |
If present, this is a symlink object.
The value is the relative path of the target object in the
format <container>/<object>.
in: header
required: false
type: string
X-Timestamp:
description: |
The date and time in `UNIX Epoch time stamp
@ -1092,6 +1130,23 @@ swiftinfo_sig:
in: query
required: false
type: string
symlink:
description: |
If you include the ``symlink=get`` query parameter
and the object is a symlink, then the response will include
data and metadata from the symlink itself rather than from the target.
in: query
required: false
type: string
symlink_copy:
description: |
If you include the ``symlink=get`` query parameter
and the object is a symlink, the target object
contents are not copied. Instead, the symlink is copied to
create a new symlink to the same target.
in: query
required: false
type: string
temp_url_expires:
description: |
The date and time in `UNIX Epoch time stamp
@ -1180,5 +1235,12 @@ name_in_container_get:
in: body
required: true
type: string
symlink_path:
description: |
This field exists only when the object is symlink.
This is the target path of the symlink object.
in: body
required: true
type: string

View File

@ -96,6 +96,7 @@ Response Parameters
- content_type: content_type
- bytes: bytes_in_container_get
- name: name_in_container_get
- symlink_path: symlink_path
Response Example format=json

View File

@ -111,6 +111,7 @@ Request
- temp_url_expires: temp_url_expires
- filename: filename
- multipart-manifest: multipart-manifest_get
- symlink: symlink
- Range: Range
- If-Match: If-Match
- If-None-Match: If-None-Match-get-request
@ -139,7 +140,8 @@ Response Parameters
- X-Openstack-Request-Id: X-Openstack-Request-Id
- Date: Date
- X-Static-Large-Object: X-Static-Large-Object
- X-Symlink-Target: X-Symlink-Target_resp
- X-Symlink-Target-Account: X-Symlink-Target-Account_resp
Response Example
@ -263,6 +265,8 @@ Request
- X-Object-Meta-name: X-Object-Meta-name
- If-None-Match: If-None-Match-put-request
- X-Trans-Id-Extra: X-Trans-Id-Extra
- X-Symlink-Target: X-Symlink-Target
- X-Symlink-Target-Account: X-Symlink-Target-Account
Response Parameters
@ -321,6 +325,12 @@ The new object contains the same manifest as the original.
The segment objects are not copied. Instead, both the original
and new manifest objects share the same set of segment objects.
To copy a symlink either with a COPY or a PUT with the
``X-Copy-From`` request, include the ``symlink=get`` query string.
The new symlink will have the same target as the original.
The target object is not copied. Instead, both the original
and new symlinks point to the same target object.
All metadata is
preserved during the object copy. If you specify metadata on the
request to copy the object, either PUT or COPY , the metadata
@ -396,6 +406,7 @@ Request
- container: container
- object: object
- multipart-manifest: multipart-manifest_copy
- symlink: symlink_copy
- X-Auth-Token: X-Auth-Token
- X-Service-Token: X-Service-Token
- Destination: Destination
@ -445,6 +456,9 @@ manifest=delete`` query parameter. This operation deletes the
segment objects and, if all deletions succeed, this operation
deletes the manifest object.
A DELETE request made to a symlink path will delete the symlink
rather than the target object.
An alternative to using the DELETE operation is to use
the POST operation with the ``bulk-delete`` query parameter.
@ -570,6 +584,7 @@ Request
- temp_url_expires: temp_url_expires
- filename: filename
- multipart-manifest: multipart-manifest_head
- symlink: symlink
- X-Newest: X-Newest
- If-Match: If-Match
- If-None-Match: If-None-Match-get-request
@ -597,7 +612,8 @@ Response Parameters
- Date: Date
- X-Static-Large-Object: X-Static-Large-Object
- Content-Type: Content-Type_obj_resp
- X-Symlink-Target: X-Symlink-Target_resp
- X-Symlink-Target-Account: X-Symlink-Target-Account_resp
Response Example
@ -659,6 +675,15 @@ body. There are alternate uses of the POST operation as follows:
can be used to upload an archive (tar file). The archive is then extracted
to create objects.
A POST request must not include X-Symlink-Target header. If it does then a
400 status code is returned and the object metadata is not modified.
When a POST request is sent to a symlink, the metadata will be applied to the
symlink, but the request will result in a ``307 Temporary Redirect`` response
to the client. The POST is never redirected to the target object, thus a
GET/HEAD request to the symlink without ``symlink=get`` will not return the
metadata that was sent as part of the POST request.
Example requests and responses:
- Create object metadata:

View File

@ -6,15 +6,22 @@ gcc [platform:rpm]
gettext [!platform:suse]
gettext-runtime [platform:suse]
liberasurecode-dev [platform:dpkg]
liberasurecode-devel [platform:rpm]
# There's no library in CentOS 7 but Fedora and openSUSE have it.
liberasurecode-devel [platform:rpm !platform:centos]
libffi-dev [platform:dpkg]
libffi-devel [platform:rpm]
memcached
python-dev [platform:dpkg]
python-devel [platform:rpm]
python3-dev [platform:dpkg]
python34-devel [platform:redhat]
python3-devel [platfrom:suse]
python3-devel [platform:fedora platform:suse]
# python3-devel does not pull in the python3 package on openSUSE so
# we need to be explicit. The python3 package contains the XML module
# which is required by a python3 virtualenv. Similarly, in python2,
# the XML module is located in python-xml which is not pulled in
# by python-devel as well. See https://bugzilla.suse.com/show_bug.cgi?id=1046990
python3 [platform:suse]
python-xml [platform:suse]
rsync
xfsprogs
libssl-dev [platform:dpkg]

View File

@ -353,7 +353,7 @@ Logging address. The default is /dev/log.
.IP \fBinterval\fR
Minimum time for a pass to take. The default is 300 seconds.
.IP \fBconcurrency\fR
Number of reaper workers to spawn. The default is 4.
Number of updater workers to spawn. The default is 4.
.IP \fBnode_timeout\fR
Request timeout to external services. The default is 3 seconds.
.IP \fBconn_timeout\fR

View File

@ -211,7 +211,7 @@ The default is 'expiring_objects'.
.IP \fBreport_interval\fR
The default is 300 seconds.
.IP \fBconcurrency\fR
Number of replication workers to spawn. The default is 1.
Number of expirer workers to spawn. The default is 1.
.IP \fBprocesses\fR
Processes is how many parts to divide the work into, one part per process that will be doing the work.
Processes set 0 means that a single process will be doing all the work.

View File

@ -495,7 +495,7 @@ Logging address. The default is /dev/log.
.IP \fBinterval\fR
Minimum time for a pass to take. The default is 300 seconds.
.IP \fBconcurrency\fR
Number of reaper workers to spawn. The default is 1.
Number of updater workers to spawn. The default is 1.
.IP \fBnode_timeout\fR
Request timeout to external services. The default is 10 seconds.
.IP \fBobjects_per_second\fR
@ -546,7 +546,7 @@ system specs. 0 is unlimited. The default is 20.
Maximum bytes audited per second. Should be tuned according to individual
system specs. 0 is unlimited. The default is 10000000.
.IP \fBconcurrency\fR
Number of reaper workers to spawn. The default is 1.
Number of auditor workers to spawn. The default is 1.
.IP \fBlog_time\fR
The default is 3600 seconds.
.IP \fBzero_byte_files_per_second\fR

View File

@ -82,10 +82,12 @@ Get cluster socket usage stats
Get drive audit error stats
.IP "\fB-T, --time\fR"
Check time synchronization
.IP "\fB--swift-versions\fR"
Check swift version
.IP "\fB--all\fR"
Perform all checks. Equivalent to \-arudlqT
\-\-md5 \-\-sockstat \-\-auditor \-\-updater \-\-expirer
\-\-driveaudit \-\-validate\-servers
\-\-driveaudit \-\-validate\-servers \-\-swift-versions
.IP "\fB--region=REGION\fR"
Only query servers in specified region
.IP "\fB-z ZONE, --zone=ZONE\fR"

View File

@ -9,7 +9,7 @@ eventlet_debug = true
[pipeline:main]
# Yes, proxy-logging appears twice. This is so that
# middleware-originated requests get logged too.
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache listing_formats bulk tempurl ratelimit crossdomain container_sync tempauth staticweb copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache listing_formats bulk tempurl ratelimit crossdomain container_sync tempauth staticweb copy container-quotas account-quotas slo dlo versioned_writes symlink proxy-logging proxy-server
[filter:catch_errors]
use = egg:swift#catch_errors
@ -74,6 +74,9 @@ use = egg:swift#copy
[filter:listing_formats]
use = egg:swift#listing_formats
[filter:symlink]
use = egg:swift#symlink
[app:proxy-server]
use = egg:swift#proxy
allow_account_management = true

View File

@ -87,7 +87,9 @@ The Object Storage system organizes data in a hierarchy, as follows:
object.
- Upload objects directly to the Object Storage system from a
browser by using form **POST** middleware
browser by using form **POST** middleware.
- Create symbolic links to other objects.
The account, container, and object hierarchy affects the way you
interact with the Object Storage API.

View File

@ -49,6 +49,12 @@ returns the following values for this header,
``X-Object-Meta-*`` for objects)
* headers listed in ``X-Container-Meta-Access-Control-Expose-Headers``
.. note::
An OPTIONS request to a symlink object will respond with the options for
the symlink only, the request will not be redirected to the target object.
Therefore, if the symlink's target object is in another container with
CORS settings, the response will not reflect the settings.
-----------------
Sample Javascript

View File

@ -1701,11 +1701,6 @@ etc/proxy-server.conf-sample in the source code repository.
The following configuration sections are available:
An example Account Server configuration can be found at
etc/account-server.conf-sample in the source code repository.
The following configuration sections are available:
* :ref:`[DEFAULT] <proxy_server_default_options>`
* `[proxy-server]`_
* Individual sections for `Proxy middlewares`_

View File

@ -104,6 +104,7 @@ KS :ref:`keystoneauth`
RL :ref:`ratelimit`
VW :ref:`versioned_writes`
SSC :ref:`copy`
SYM :ref:`symlink`
======================= =============================

View File

@ -244,6 +244,15 @@ StaticWeb
:members:
:show-inheritance:
.. _symlink:
Symlink
=======
.. automodule:: swift.common.middleware.symlink
:members:
:show-inheritance:
.. _common_tempauth:
TempAuth

View File

@ -36,9 +36,15 @@ synchronization key.
.. note::
If you are using encryption middleware in the cluster from which objects
are being synced, then you should follow the instructions to configure
are being synced, then you should follow the instructions for
:ref:`container_sync_client_config` to be compatible with encryption.
.. note::
If you are using symlink middleware in the cluster from which objects
are being synced, then you should follow the instructions for
:ref:`symlink_container_sync_client_config` to be compatible with symlinks.
--------------------------
Configuring Container Sync
--------------------------
@ -440,7 +446,7 @@ then a symlink to the container database is created in a sync-containers
sub-directory on the same device.
Similarly, when the container sync metadata keys are deleted, the container
server and container-replicator would take care of deleting the symlinks
server and container-replicator would take care of deleting the symlinks
from ``sync-containers``.
.. note::

View File

@ -24,13 +24,17 @@
# log_statsd_metric_prefix =
[pipeline:main]
pipeline = catch_errors proxy-logging cache proxy-server
pipeline = catch_errors proxy-logging cache symlink proxy-server
[app:proxy-server]
use = egg:swift#proxy
account_autocreate = true
# See proxy-server.conf-sample for options
[filter:symlink]
use = egg:swift#symlink
# See proxy-server.conf-sample for options
[filter:cache]
use = egg:swift#memcache
# See proxy-server.conf-sample for options

View File

@ -94,12 +94,12 @@ bind_port = 8080
[pipeline:main]
# This sample pipeline uses tempauth and is used for SAIO dev work and
# testing. See below for a pipeline using keystone.
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache listing_formats container_sync bulk tempurl ratelimit tempauth copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
pipeline = catch_errors gatekeeper healthcheck proxy-logging cache listing_formats container_sync bulk tempurl ratelimit tempauth copy container-quotas account-quotas slo dlo versioned_writes symlink proxy-logging proxy-server
# The following pipeline shows keystone integration. Comment out the one
# above and uncomment this one. Additional steps for integrating keystone are
# covered further below in the filter sections for authtoken and keystoneauth.
#pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit authtoken keystoneauth copy container-quotas account-quotas slo dlo versioned_writes proxy-logging proxy-server
#pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl ratelimit authtoken keystoneauth copy container-quotas account-quotas slo dlo versioned_writes symlink proxy-logging proxy-server
[app:proxy-server]
use = egg:swift#proxy
@ -869,12 +869,6 @@ use = egg:swift#versioned_writes
# If you don't put it in the pipeline, it will be inserted for you.
[filter:copy]
use = egg:swift#copy
# By default object POST requests update metadata without modification of the
# original data file
# Set this to True to enable the old, slow behavior wherein object POST
# requests are transformed into COPY requests where source and destination are
# the same. All client-visible behavior (save response time) should be
# identical.
# Note: To enable encryption, add the following 2 dependent pieces of crypto
# middleware to the proxy-server pipeline. They should be to the right of all
@ -934,3 +928,13 @@ use = egg:swift#encryption
# be automatically inserted for you.
[filter:listing_formats]
use = egg:swift#listing_formats
# Note: Put after slo, dlo, versioned_writes, but before encryption in the
# pipeline.
[filter:symlink]
use = egg:swift#symlink
# Symlinks can point to other symlinks provided the number of symlinks in a
# chain does not exceed the symloop_max value. If the number of chained
# symlinks exceeds the limit symloop_max a 409 (HTTPConflict) error
# response will be produced.
# symloop_max = 2

View File

@ -107,6 +107,7 @@ paste.filter_factory =
encryption = swift.common.middleware.crypto:filter_factory
kms_keymaster = swift.common.middleware.crypto.kms_keymaster:filter_factory
listing_formats = swift.common.middleware.listing_formats:filter_factory
symlink = swift.common.middleware.symlink:filter_factory
[build_sphinx]
all_files = 1

View File

@ -906,6 +906,41 @@ class SwiftRecon(object):
matches, len(hosts), errors))
print("=" * 79)
def version_check(self, hosts):
"""
Check OS Swift version of hosts. Inform if differs.
:param hosts: set of hosts to check. in the format of:
set([('127.0.0.1', 6020), ('127.0.0.2', 6030)])
"""
versions = set()
errors = 0
print("[%s] Checking versions" % self._ptime())
recon = Scout("version", self.verbose, self.suppress_errors,
self.timeout)
for url, response, status, ts_start, ts_end in self.pool.imap(
recon.scout, hosts):
if status != 200:
errors = errors + 1
continue
versions.add(response['version'])
if self.verbose:
print("-> %s installed version %s" % (
url, response['version']))
if not len(versions):
print("No hosts returned valid data.")
elif len(versions) == 1:
print("Versions matched (%s), "
"%s error[s] while checking hosts." % (
versions.pop(), errors))
else:
print("Versions not matched (%s), "
"%s error[s] while checking hosts." % (
", ".join(sorted(versions)), errors))
print("=" * 79)
def _get_ring_names(self, policy=None):
"""
Retrieve name of ring files.
@ -982,6 +1017,8 @@ class SwiftRecon(object):
help="Check time synchronization")
args.add_option('--jitter', type="float", default=0.0,
help="Maximal allowed time jitter")
args.add_option('--swift-versions', action="store_true",
help="Check swift versions")
args.add_option('--top', type='int', metavar='COUNT', default=0,
help='Also show the top COUNT entries in rank order.')
args.add_option('--lowest', type='int', metavar='COUNT', default=0,
@ -990,7 +1027,7 @@ class SwiftRecon(object):
args.add_option('--all', action="store_true",
help="Perform all checks. Equal to \t\t\t-arudlqT "
"--md5 --sockstat --auditor --updater --expirer "
"--driveaudit --validate-servers")
"--driveaudit --validate-servers --swift-versions")
args.add_option('--region', type="int",
help="Only query servers in specified region")
args.add_option('--zone', '-z', type="int",
@ -1063,6 +1100,7 @@ class SwiftRecon(object):
self.server_type_check(hosts)
self.driveaudit_check(hosts)
self.time_check(hosts, options.jitter)
self.version_check(hosts)
else:
if options.async:
if self.server_type == 'object':
@ -1112,6 +1150,8 @@ class SwiftRecon(object):
self.driveaudit_check(hosts)
if options.time:
self.time_check(hosts, options.jitter)
if options.swift_versions:
self.version_check(hosts)
def main():

View File

@ -898,7 +898,9 @@ swift-ring-builder <builder_file> rebalance [options]
min_part_seconds_left = builder.min_part_seconds_left
try:
last_balance = builder.get_balance()
last_dispersion = builder.dispersion
parts, balance, removed_devs = builder.rebalance(seed=get_seed(3))
dispersion = builder.dispersion
except exceptions.RingBuilderError as e:
print('-' * 79)
print("An error has occurred during ring validation. Common\n"
@ -922,9 +924,25 @@ swift-ring-builder <builder_file> rebalance [options]
# special value(MAX_BALANCE) until zero weighted device return all
# its partitions. So we cannot check balance has changed.
# Thus we need to check balance or last_balance is special value.
if not options.force and \
not devs_changed and abs(last_balance - balance) < 1 and \
not (last_balance == MAX_BALANCE and balance == MAX_BALANCE):
be_cowardly = True
if options.force:
# User said save it, so we save it.
be_cowardly = False
elif devs_changed:
# We must save if a device changed; this could be something like
# a changed IP address.
be_cowardly = False
else:
# If balance or dispersion changed (presumably improved), then
# we should save to get the improvement.
balance_changed = (
abs(last_balance - balance) >= 1 or
(last_balance == MAX_BALANCE and balance == MAX_BALANCE))
dispersion_changed = abs(last_dispersion - dispersion) >= 1
if balance_changed or dispersion_changed:
be_cowardly = False
if be_cowardly:
print('Cowardly refusing to save rebalance as it did not change '
'at least 1%.')
exit(EXIT_WARNING)
@ -1085,14 +1103,16 @@ swift-ring-builder <builder_file> write_ring
'set_info' calls when no rebalance is needed but you want to send out the
new device information.
"""
if not builder.devs:
print('Unable to write empty ring.')
exit(EXIT_ERROR)
ring_data = builder.get_ring()
if not ring_data._replica2part2dev_id:
if ring_data.devs:
print('Warning: Writing a ring with no partition '
'assignments but with devices; did you forget to run '
'"rebalance"?')
else:
print('Warning: Writing an empty ring')
ring_data.save(
pathjoin(backup_dir, '%d.' % time() + basename(ring_file)))
ring_data.save(ring_file)

View File

@ -199,6 +199,10 @@ class SegmentError(SwiftException):
pass
class LinkIterError(SwiftException):
pass
class ReplicationException(Exception):
pass

View File

@ -157,7 +157,8 @@ class InternalClient(object):
'auto_create_account_prefix', default='.')
def make_request(
self, method, path, headers, acceptable_statuses, body_file=None):
self, method, path, headers, acceptable_statuses, body_file=None,
params=None):
"""Makes a request to Swift with retries.
:param method: HTTP method of request.
@ -166,6 +167,8 @@ class InternalClient(object):
:param acceptable_statuses: List of acceptable statuses for request.
:param body_file: Body file to be passed along with request,
defaults to None.
:param params: A dict of params to be set in request query string,
defaults to None.
:returns: Response object on success.
@ -185,6 +188,8 @@ class InternalClient(object):
if hasattr(body_file, 'seek'):
body_file.seek(0)
req.body_file = body_file
if params:
req.params = params
try:
resp = req.get_response(self.app)
if resp.status_int in acceptable_statuses or \
@ -606,14 +611,30 @@ class InternalClient(object):
headers=headers)
def get_object(self, account, container, obj, headers,
acceptable_statuses=(2,)):
acceptable_statuses=(2,), params=None):
"""
Returns a 3-tuple (status, headers, iterator of object body)
Gets an object.
:param account: The object's account.
:param container: The object's container.
:param obj: The object name.
:param headers: Headers to send with request, defaults to empty dict.
:param acceptable_statuses: List of status for valid responses,
defaults to (2,).
:param params: A dict of params to be set in request query string,
defaults to None.
:raises UnexpectedResponse: Exception raised when requests fail
to get a response with an acceptable status
:raises Exception: Exception is raised when code fails in an
unexpected way.
:returns: A 3-tuple (status, headers, iterator of object body)
"""
headers = headers or {}
path = self.make_path(account, container, obj)
resp = self.make_request('GET', path, headers, acceptable_statuses)
resp = self.make_request(
'GET', path, headers, acceptable_statuses, params=params)
return (resp.status_int, resp.headers, resp.app_iter)
def iter_object_lines(
@ -697,7 +718,7 @@ class InternalClient(object):
:param account: The object's account.
:param container: The object's container.
:param obj: The object.
:param headers: Headers to send with request, defaults ot empty dict.
:param headers: Headers to send with request, defaults to empty dict.
:raises UnexpectedResponse: Exception raised when requests fail
to get a response with an acceptable status

View File

@ -114,45 +114,18 @@ greater than 5GB.
"""
import os
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
from six.moves.urllib.parse import quote, unquote
from six.moves.urllib.parse import quote
from swift.common import utils
from swift.common.utils import get_logger, \
config_true_value, FileLikeIter, read_conf_dir, close_if_possible
from swift.common.utils import get_logger, config_true_value, FileLikeIter, \
close_if_possible
from swift.common.swob import Request, HTTPPreconditionFailed, \
HTTPRequestEntityTooLarge, HTTPBadRequest, HTTPException
from swift.common.http import HTTP_MULTIPLE_CHOICES, is_success, HTTP_OK
from swift.common.constraints import check_account_format, MAX_FILE_SIZE
from swift.common.request_helpers import copy_header_subset, remove_items, \
is_sys_meta, is_sys_or_user_meta, is_object_transient_sysmeta
from swift.common.wsgi import WSGIContext, make_subrequest
def _check_path_header(req, name, length, error_msg):
"""
Validate that the value of path-like header is
well formatted. We assume the caller ensures that
specific header is present in req.headers.
:param req: HTTP request object
:param name: header name
:param length: length of path segment check
:param error_msg: error message for client
:returns: A tuple with path parts according to length
:raise HTTPPreconditionFailed: if header value
is not well formatted.
"""
src_header = unquote(req.headers.get(name))
if not src_header.startswith('/'):
src_header = '/' + src_header
try:
return utils.split_path(src_header, length, length, True)
except ValueError:
raise HTTPPreconditionFailed(
request=req,
body=error_msg)
is_sys_meta, is_sys_or_user_meta, is_object_transient_sysmeta, \
check_path_header
from swift.common.wsgi import WSGIContext, make_subrequest, load_app_config
def _check_copy_from_header(req):
@ -166,9 +139,9 @@ def _check_copy_from_header(req):
:raise HTTPPreconditionFailed: if x-copy-from value
is not well formatted.
"""
return _check_path_header(req, 'X-Copy-From', 2,
'X-Copy-From header must be of the form '
'<container name>/<object name>')
return check_path_header(req, 'X-Copy-From', 2,
'X-Copy-From header must be of the form '
'<container name>/<object name>')
def _check_destination_header(req):
@ -182,9 +155,9 @@ def _check_destination_header(req):
:raise HTTPPreconditionFailed: if destination value
is not well formatted.
"""
return _check_path_header(req, 'Destination', 2,
'Destination header must be of the form '
'<container name>/<object name>')
return check_path_header(req, 'Destination', 2,
'Destination header must be of the form '
'<container name>/<object name>')
def _copy_headers(src, dest):
@ -263,25 +236,9 @@ class ServerSideCopyMiddleware(object):
# This takes preference over the one set in proxy app
return
cp = ConfigParser()
if os.path.isdir(conf['__file__']):
read_conf_dir(cp, conf['__file__'])
else:
cp.read(conf['__file__'])
try:
pipe = cp.get("pipeline:main", "pipeline")
except (NoSectionError, NoOptionError):
return
proxy_name = pipe.rsplit(None, 1)[-1]
proxy_section = "app:" + proxy_name
try:
conf['object_post_as_copy'] = cp.get(proxy_section,
'object_post_as_copy')
except (NoSectionError, NoOptionError):
pass
proxy_conf = load_app_config(conf['__file__'])
if 'object_post_as_copy' in proxy_conf:
conf['object_post_as_copy'] = proxy_conf['object_post_as_copy']
def __call__(self, env, start_response):
req = Request(env)

View File

@ -103,13 +103,14 @@ class BaseDecrypterContext(CryptoWSGIContext):
to be decrypted but crypto meta was not
found.
"""
value, crypto_meta = extract_crypto_meta(value)
extracted_value, crypto_meta = extract_crypto_meta(value)
if crypto_meta:
self.crypto.check_crypto_meta(crypto_meta)
value = self.decrypt_value(value, key, crypto_meta)
value = self.decrypt_value(extracted_value, key, crypto_meta)
elif required:
raise EncryptionException(
"Missing crypto meta in value %s" % value)
return value
def decrypt_value(self, value, key, crypto_meta):

View File

@ -12,17 +12,13 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import hashlib
import hmac
import os
import string
import six
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
from swift.common.swob import Request, HTTPException
from swift.common.utils import readconf
from swift.common.utils import readconf, strict_b64decode
from swift.common.wsgi import WSGIContext
@ -141,17 +137,12 @@ class KeyMaster(object):
conf = readconf(self.keymaster_config_path, 'keymaster')
b64_root_secret = conf.get('encryption_root_secret')
try:
# b64decode will silently discard bad characters, but we should
# treat them as an error
if not isinstance(b64_root_secret, six.string_types) or any(
c not in string.digits + string.ascii_letters + '/+\r\n'
for c in b64_root_secret.strip('\r\n=')):
raise ValueError
binary_root_secret = base64.b64decode(b64_root_secret)
binary_root_secret = strict_b64decode(b64_root_secret,
allow_line_breaks=True)
if len(binary_root_secret) < 32:
raise ValueError
return binary_root_secret
except (TypeError, ValueError):
except ValueError:
raise ValueError(
'encryption_root_secret option in %s must be a base64 '
'encoding of at least 32 raw bytes' % (

View File

@ -119,10 +119,8 @@ Here's an example using ``curl`` with tiny 1-byte segments::
"""
import json
import os
import six
from six.moves.configparser import ConfigParser, NoSectionError, NoOptionError
from six.moves.urllib.parse import unquote
from hashlib import md5
@ -132,10 +130,9 @@ from swift.common.http import is_success
from swift.common.swob import Request, Response, \
HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict
from swift.common.utils import get_logger, \
RateLimitedIterator, read_conf_dir, 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.wsgi import WSGIContext, make_subrequest
from swift.common.wsgi import WSGIContext, make_subrequest, load_app_config
class GetContext(WSGIContext):
@ -381,26 +378,12 @@ class DynamicLargeObject(object):
'__file__' not in conf):
return
cp = ConfigParser()
if os.path.isdir(conf['__file__']):
read_conf_dir(cp, conf['__file__'])
else:
cp.read(conf['__file__'])
try:
pipe = cp.get("pipeline:main", "pipeline")
except (NoSectionError, NoOptionError):
return
proxy_name = pipe.rsplit(None, 1)[-1]
proxy_section = "app:" + proxy_name
proxy_conf = load_app_config(conf['__file__'])
for setting in ('rate_limit_after_segment',
'rate_limit_segments_per_sec',
'max_get_time'):
try:
conf[setting] = cp.get(proxy_section, setting)
except (NoSectionError, NoOptionError):
pass
if setting in proxy_conf:
conf[setting] = proxy_conf[setting]
def __call__(self, env, start_response):
"""

View File

@ -92,6 +92,7 @@ def container_to_xml(listing, base_name):
'last_modified'):
SubElement(sub, field).text = six.text_type(
record.pop(field))
return tostring(doc, encoding='UTF-8').replace(
"<?xml version='1.0' encoding='UTF-8'?>",
'<?xml version="1.0" encoding="UTF-8"?>', 1)

View File

@ -0,0 +1,579 @@
# Copyright (c) 2010-2017 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Symlink Middleware
Symlinks are objects stored in Swift that contain a reference to another
object (hereinafter, this is called "target object"). They are analogous to
symbolic links in Unix-like operating systems. The existence of a symlink
object does not affect the target object in any way. An important use case is
to use a path in one container to access an object in a different container,
with a different policy. This allows policy cost/performance trade-offs to be
made on individual objects.
Clients create a Swift symlink by performing a zero-length PUT request
with the header ``X-Symlink-Target: <container>/<object>``. For a cross-account
symlink, the header ``X-Symlink-Target-Account: <account>`` must be included.
If omitted, it is inserted automatically with the account of the symlink
object in the PUT request process.
Symlinks must be zero-byte objects. Attempting to PUT a symlink
with a non-empty request body will result in a 400-series error. Also, POST
with X-Symlink-Target header always results in a 400-series error. The target
object need not exist at symlink creation time. It is suggested to set the
``Content-Type`` of symlink objects to a distinct value such as
``application/symlink``.
A GET/HEAD request to a symlink will result in a request to the target
object referenced by the symlink's ``X-Symlink-Target-Account`` and
``X-Symlink-Target`` headers. The response of the GET/HEAD request will contain
a ``Content-Location`` header with the path location of the target object. A
GET/HEAD request to a symlink with the query parameter ``?symlink=get`` will
result in the request targeting the symlink itself.
A symlink can point to another symlink. Chained symlinks will be traversed
until target is not a symlink. If the number of chained symlinks exceeds the
limit ``symloop_max`` an error response will be produced. The value of
``symloop_max`` can be defined in the symlink config section of
`proxy-server.conf`. If not specified, the default ``symloop_max`` value is 2.
If a value less than 1 is specified, the default value will be used.
A HEAD/GET request to a symlink object behaves as a normal HEAD/GET request
to the target object. Therefore issuing a HEAD request to the symlink will
return the target metadata, and issuing a GET request to the symlink will
return the data and metadata of the target object. To return the symlink
metadata (with its empty body) a GET/HEAD request with the ``?symlink=get``
query parameter must be sent to a symlink object.
A POST request to a symlink will result in a 307 TemporaryRedirect response.
The response will contain a ``Location`` header with the path of the target
object as the value. The request is never redirected to the target object by
Swift. Nevertheless, the metadata in the POST request will be applied to the
symlink because object servers cannot know for sure if the current object is a
symlink or not in eventual consistency.
A DELETE request to a symlink will delete the symlink itself. The target
object will not be deleted.
A COPY request, or a PUT request with a ``X-Copy-From`` header, to a symlink
will copy the target object. The same request to a symlink with the query
parameter ``?symlink=get`` will copy the symlink itself.
An OPTIONS request to a symlink will respond with the options for the symlink
only, the request will not be redirected to the target object. Please note that
if the symlink's target object is in another container with CORS settings, the
response will not reflect the settings.
Tempurls can be used to GET/HEAD symlink objects, but PUT is not allowed and
will result in a 400-series error. The GET/HEAD tempurls honor the scope of
the tempurl key. Container tempurl will only work on symlinks where the target
container is the same as the symlink. In case a symlink targets an object
in a different container, a GET/HEAD request will result in a 401 Unauthorized
error. The account level tempurl will allow cross container symlinks.
If a symlink object is overwritten while it is in a versioned container, the
symlink object itself is versioned, not the referenced object.
A GET request with query parameter ``?format=json`` to a container which
contains symlinks will respond with additional information ``symlink_path``
for each symlink object in the container listing. The ``symlink_path`` value
is the target path of the symlink. Clients can differentiate symlinks and
other objects by this function. Note that responses of any other format
(e.g.``?format=xml``) won't include ``symlink_path`` info.
Errors
* PUT with the header ``X-Symlink-Target`` with non-zero Content-Length
will produce a 400 BadRequest error.
* POST with the header ``X-Symlink-Target`` will produce a
400 BadRequest error.
* GET/HEAD traversing more than ``symloop_max`` chained symlinks will
produce a 409 Conflict error.
* POSTs will produce a 307 TemporaryRedirect error.
----------
Deployment
----------
Symlinks are enabled by adding the `symlink` middleware to the proxy server
WSGI pipeline and including a corresponding filter configuration section in the
`proxy-server.conf` file. The `symlink` middleware should be placed after
`slo`, `dlo` and `versioned_writes` middleware, but before `encryption`
middleware in the pipeline. See the `proxy-server.conf-sample` file for further
details. :ref:`Additional steps <symlink_container_sync_client_config>` are
required if the container sync feature is being used.
.. note::
Once you have deployed `symlink` middleware in your pipeline, you should
neither remove the `symlink` middleware nor downgrade swift to a version
earlier than symlinks being supported. Doing so may result in unexpected
container listing results in addition to symlink objects behaving like a
normal object.
.. _symlink_container_sync_client_config:
Container sync configuration
----------------------------
If container sync is being used then the `symlink` middleware
must be added to the container sync internal client pipeline. The following
configuration steps are required:
#. Create a custom internal client configuration file for container sync (if
one is not already in use) based on the sample file
`internal-client.conf-sample`. For example, copy
`internal-client.conf-sample` to `/etc/swift/container-sync-client.conf`.
#. Modify this file to include the `symlink` middleware in the pipeline in
the same way as described above for the proxy server.
#. Modify the container-sync section of all container server config files to
point to this internal client config file using the
``internal_client_conf_path`` option. For example::
internal_client_conf_path = /etc/swift/container-sync-client.conf
.. note::
These container sync configuration steps will be necessary for container
sync probe tests to pass if the `symlink` middleware is included in the
proxy pipeline of a test cluster.
"""
import json
import os
from cgi import parse_header
from six.moves.urllib.parse import unquote
from swift.common.utils import get_logger, register_swift_info, split_path, \
MD5_OF_EMPTY_STRING, closing_if_possible
from swift.common.constraints import check_account_format
from swift.common.wsgi import WSGIContext, make_subrequest
from swift.common.request_helpers import get_sys_meta_prefix, \
check_path_header
from swift.common.swob import Request, HTTPBadRequest, HTTPTemporaryRedirect, \
HTTPException, HTTPConflict, HTTPPreconditionFailed
from swift.common.http import is_success
from swift.common.exceptions import LinkIterError
from swift.common.header_key_dict import HeaderKeyDict
DEFAULT_SYMLOOP_MAX = 2
# Header values for symlink target path strings will be quoted values.
TGT_OBJ_SYMLINK_HDR = 'x-symlink-target'
TGT_ACCT_SYMLINK_HDR = 'x-symlink-target-account'
TGT_OBJ_SYSMETA_SYMLINK_HDR = get_sys_meta_prefix('object') + 'symlink-target'
TGT_ACCT_SYSMETA_SYMLINK_HDR = \
get_sys_meta_prefix('object') + 'symlink-target-account'
def _check_symlink_header(req):
"""
Validate that the value from x-symlink-target header is
well formatted. We assume the caller ensures that
x-symlink-target header is present in req.headers.
:param req: HTTP request object
:raise: HTTPPreconditionFailed if x-symlink-target value
is not well formatted.
:raise: HTTPBadRequest if the x-symlink-target value points to the request
path.
"""
# N.B. check_path_header doesn't assert the leading slash and
# copy middleware may accept the format. In the symlink, API
# says apparently to use "container/object" format so add the
# validation first, here.
if unquote(req.headers[TGT_OBJ_SYMLINK_HDR]).startswith('/'):
raise HTTPPreconditionFailed(
body='X-Symlink-Target header must be of the '
'form <container name>/<object name>',
request=req, content_type='text/plain')
# check container and object format
container, obj = check_path_header(
req, TGT_OBJ_SYMLINK_HDR, 2,
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
# Check account format if it exists
account = check_account_format(
req, unquote(req.headers[TGT_ACCT_SYMLINK_HDR])) \
if TGT_ACCT_SYMLINK_HDR in req.headers else None
# Extract request path
_junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True)
if not account:
account = req_acc
# Check if symlink targets the symlink itself or not
if (account, container, obj) == (req_acc, req_cont, req_obj):
raise HTTPBadRequest(
body='Symlink cannot target itself',
request=req, content_type='text/plain')
def symlink_usermeta_to_sysmeta(headers):
"""
Helper function to translate from X-Symlink-Target and
X-Symlink-Target-Account to X-Object-Sysmeta-Symlink-Target
and X-Object-Sysmeta-Symlink-Target-Account.
:param headers: request headers dict. Note that the headers dict
will be updated directly.
"""
# To preseve url-encoded value in the symlink header, use raw value
if TGT_OBJ_SYMLINK_HDR in headers:
headers[TGT_OBJ_SYSMETA_SYMLINK_HDR] = headers.pop(
TGT_OBJ_SYMLINK_HDR)
if TGT_ACCT_SYMLINK_HDR in headers:
headers[TGT_ACCT_SYSMETA_SYMLINK_HDR] = headers.pop(
TGT_ACCT_SYMLINK_HDR)
def symlink_sysmeta_to_usermeta(headers):
"""
Helper function to translate from X-Object-Sysmeta-Symlink-Target and
X-Object-Sysmeta-Symlink-Target-Account to X-Symlink-Target and
X-Sysmeta-Symlink-Target-Account
:param headers: request headers dict. Note that the headers dict
will be updated directly.
"""
if TGT_OBJ_SYSMETA_SYMLINK_HDR in headers:
headers[TGT_OBJ_SYMLINK_HDR] = headers.pop(
TGT_OBJ_SYSMETA_SYMLINK_HDR)
if TGT_ACCT_SYSMETA_SYMLINK_HDR in headers:
headers[TGT_ACCT_SYMLINK_HDR] = headers.pop(
TGT_ACCT_SYSMETA_SYMLINK_HDR)
class SymlinkContainerContext(WSGIContext):
def __init__(self, wsgi_app, logger):
super(SymlinkContainerContext, self).__init__(wsgi_app)
self.logger = logger
def handle_container(self, req, start_response):
"""
Handle container requests.
:param req: a :class:`~swift.common.swob.Request`
:param start_response: start_response function
:return: Response Iterator after start_response called.
"""
app_resp = self._app_call(req.environ)
if req.method == 'GET' and is_success(self._get_status_int()):
app_resp = self._process_json_resp(app_resp, req)
start_response(self._response_status, self._response_headers,
self._response_exc_info)
return app_resp
def _process_json_resp(self, resp_iter, req):
"""
Iterate through json body looking for symlinks and modify its content
:return: modified json body
"""
with closing_if_possible(resp_iter):
resp_body = ''.join(resp_iter)
body_json = json.loads(resp_body)
swift_version, account, _junk = split_path(req.path, 2, 3, True)
new_body = json.dumps(
[self._extract_symlink_path_json(obj_dict, swift_version, account)
for obj_dict in body_json])
self.update_content_length(len(new_body))
return [new_body]
def _extract_symlink_path_json(self, obj_dict, swift_version, account):
"""
Extract the symlink path from the hash value
:return: object dictionary with additional key:value pair if object
is a symlink. The new key is symlink_path.
"""
if 'hash' in obj_dict:
hash_value, meta = parse_header(obj_dict['hash'])
obj_dict['hash'] = hash_value
target = None
for key in meta:
if key == 'symlink_target':
target = meta[key]
elif key == 'symlink_target_account':
account = meta[key]
else:
# make sure to add all other (key, values) back in place
obj_dict['hash'] += '; %s=%s' % (key, meta[key])
else:
if target:
obj_dict['symlink_path'] = os.path.join(
'/', swift_version, account, target)
return obj_dict
class SymlinkObjectContext(WSGIContext):
def __init__(self, wsgi_app, logger, symloop_max):
super(SymlinkObjectContext, self).__init__(wsgi_app)
self.symloop_max = symloop_max
self.logger = logger
# N.B. _loop_count and _last_target_path are used to keep
# the statement in the _recursive_get. Hence they should not be touched
# from other resources.
self._loop_count = 0
self._last_target_path = None
def handle_get_head_symlink(self, req):
"""
Handle get/head request when client sent parameter ?symlink=get
:param req: HTTP GET or HEAD object request with param ?symlink=get
:returns: Response Iterator
"""
resp = self._app_call(req.environ)
response_header_dict = HeaderKeyDict(self._response_headers)
symlink_sysmeta_to_usermeta(response_header_dict)
self._response_headers = response_header_dict.items()
return resp
def handle_get_head(self, req):
"""
Handle get/head request and in case the response is a symlink,
redirect request to target object.
:param req: HTTP GET or HEAD object request
:returns: Response Iterator
"""
try:
return self._recursive_get_head(req)
except LinkIterError:
errmsg = 'Too many levels of symbolic links, ' \
'maximum allowed is %d' % self.symloop_max
raise HTTPConflict(
body=errmsg, request=req, content_type='text/plain')
def _recursive_get_head(self, req):
resp = self._app_call(req.environ)
def build_traversal_req(symlink_target):
"""
:returns: new request for target path if it's symlink otherwise
None
"""
version, account, _junk = split_path(req.path, 2, 3, True)
account = self._response_header_value(
TGT_ACCT_SYSMETA_SYMLINK_HDR) or account
target_path = os.path.join(
'/', version, account,
symlink_target.lstrip('/'))
self._last_target_path = target_path
new_req = make_subrequest(
req.environ, path=target_path, method=req.method,
headers=req.headers, swift_source='SYM')
new_req.headers.pop('X-Backend-Storage-Policy-Index', None)
return new_req
symlink_target = self._response_header_value(
TGT_OBJ_SYSMETA_SYMLINK_HDR)
if symlink_target:
if self._loop_count >= self.symloop_max:
raise LinkIterError()
# format: /<account name>/<container name>/<object name>
new_req = build_traversal_req(symlink_target)
self._loop_count += 1
return self._recursive_get_head(new_req)
else:
if self._last_target_path:
# Content-Location will be applied only when one or more
# symlink recursion occurred.
# In this case, Content-Location is applied to show which
# object path caused the error response.
# To preserve '%2F'(= quote('/')) in X-Symlink-Target
# header value as it is, Content-Location value comes from
# TGT_OBJ_SYMLINK_HDR, not req.path
self._response_headers.extend(
[('Content-Location', self._last_target_path)])
return resp
def handle_put(self, req):
"""
Handle put request when it contains X-Symlink-Target header.
Symlink headers are validated and moved to sysmeta namespace.
:param req: HTTP PUT object request
:returns: Response Iterator
"""
if req.content_length != 0:
raise HTTPBadRequest(
body='Symlink requests require a zero byte body',
request=req,
content_type='text/plain')
_check_symlink_header(req)
symlink_usermeta_to_sysmeta(req.headers)
# Store info in container update that this object is a symlink.
# We have a design decision to use etag space to store symlink info for
# object listing because it's immutable unless the object is
# overwritten. This may impact the downgrade scenario that the symlink
# info can be appreared as the suffix in the hash value of object
# listing result for clients.
# To create override etag easily, we have a contraint that the symlink
# must be 0 byte so we can add etag of the empty string + symlink info
# here, simply. Note that this override etag may be encrypted in the
# container db by encrypion middleware.
etag_override = [
MD5_OF_EMPTY_STRING,
'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
]
if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
etag_override.append(
'symlink_target_account=%s' %
req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR])
req.headers['X-Object-Sysmeta-Container-Update-Override-Etag'] = \
'; '.join(etag_override)
return self._app_call(req.environ)
def handle_post(self, req):
"""
Handle post request. If POSTing to a symlink, a HTTPTemporaryRedirect
error message is returned to client.
Clients that POST to symlinks should understand that the POST is not
redirected to the target object like in a HEAD/GET request. POSTs to a
symlink will be handled just like a normal object by the object server.
It cannot reject it because it may not have symlink state when the POST
lands. The object server has no knowledge of what is a symlink object
is. On the other hand, on POST requests, the object server returns all
sysmeta of the object. This method uses that sysmeta to determine if
the stored object is a symlink or not.
:param req: HTTP POST object request
:raises: HTTPTemporaryRedirect if POSTing to a symlink.
:returns: Response Iterator
"""
if TGT_OBJ_SYMLINK_HDR in req.headers:
raise HTTPBadRequest(
body='A PUT request is required to set a symlink target',
request=req,
content_type='text/plain')
resp = self._app_call(req.environ)
if not is_success(self._get_status_int()):
return resp
tgt_co = self._response_header_value(TGT_OBJ_SYSMETA_SYMLINK_HDR)
if tgt_co:
version, account, _junk = req.split_path(2, 3, True)
target_acc = self._response_header_value(
TGT_ACCT_SYSMETA_SYMLINK_HDR) or account
location_hdr = os.path.join(
'/', version, target_acc, tgt_co)
req.environ['swift.leave_relative_location'] = True
errmsg = 'The requested POST was applied to a symlink. POST ' +\
'directly to the target to apply requested metadata.'
raise HTTPTemporaryRedirect(
body=errmsg, headers={'location': location_hdr})
else:
return resp
def handle_object(self, req, start_response):
"""
Handle object requests.
:param req: a :class:`~swift.common.swob.Request`
:param start_response: start_response function
:returns: Response Iterator after start_response has been called
"""
if req.method in ('GET', 'HEAD'):
# if GET request came from versioned writes, then it should get
# the symlink only, not the referenced target
if req.params.get('symlink') == 'get' or \
req.environ.get('swift.source') == 'VW':
resp = self.handle_get_head_symlink(req)
else:
resp = self.handle_get_head(req)
elif req.method == 'PUT' and (TGT_OBJ_SYMLINK_HDR in req.headers):
resp = self.handle_put(req)
elif req.method == 'POST':
resp = self.handle_post(req)
else:
# DELETE and OPTIONS reqs for a symlink and
# PUT reqs without X-Symlink-Target behave like any other object
resp = self._app_call(req.environ)
start_response(self._response_status, self._response_headers,
self._response_exc_info)
return resp
class SymlinkMiddleware(object):
"""
Middleware that implements symlinks.
Symlinks are objects stored in Swift that contain a reference to another
object (i.e., the target object). An important use case is to use a path in
one container to access an object in a different container, with a
different policy. This allows policy cost/performance trade-offs to be made
on individual objects.
"""
def __init__(self, app, conf, symloop_max):
self.app = app
self.conf = conf
self.logger = get_logger(self.conf, log_route='symlink')
self.symloop_max = symloop_max
def __call__(self, env, start_response):
req = Request(env)
try:
version, acc, cont, obj = req.split_path(3, 4, True)
except ValueError:
return self.app(env, start_response)
try:
if obj:
# object context
context = SymlinkObjectContext(self.app, self.logger,
self.symloop_max)
return context.handle_object(req, start_response)
else:
# container context
context = SymlinkContainerContext(self.app, self.logger)
return context.handle_container(req, start_response)
except HTTPException as err_resp:
return err_resp(env, start_response)
def filter_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
symloop_max = int(conf.get('symloop_max', DEFAULT_SYMLOOP_MAX))
if symloop_max < 1:
symloop_max = int(DEFAULT_SYMLOOP_MAX)
register_swift_info('symlink', symloop_max=symloop_max)
def symlink_mw(app):
return SymlinkMiddleware(app, conf, symloop_max)
return symlink_mw

View File

@ -222,7 +222,7 @@ from swift.common.utils import split_path, get_valid_utf8_str, \
register_swift_info, get_hmac, streq_const_time, quote
DISALLOWED_INCOMING_HEADERS = 'x-object-manifest'
DISALLOWED_INCOMING_HEADERS = 'x-object-manifest x-symlink-target'
#: Default headers to remove from incoming requests. Simply a whitespace
#: delimited list of header names and names can optionally end with '*' to

View File

@ -34,7 +34,8 @@ from swift.common.storage_policy import POLICIES
from swift.common.exceptions import ListingIterError, SegmentError
from swift.common.http import is_success
from swift.common.swob import HTTPBadRequest, \
HTTPServiceUnavailable, Range, is_chunked, multi_range_iterator
HTTPServiceUnavailable, Range, is_chunked, multi_range_iterator, \
HTTPPreconditionFailed
from swift.common.utils import split_path, validate_device_partition, \
close_if_possible, maybe_multipart_byteranges_to_document_iters, \
multipart_byteranges_to_document_iters, parse_content_type, \
@ -281,6 +282,31 @@ def copy_header_subset(from_r, to_r, condition):
to_r.headers[k] = v
def check_path_header(req, name, length, error_msg):
"""
Validate that the value of path-like header is
well formatted. We assume the caller ensures that
specific header is present in req.headers.
:param req: HTTP request object
:param name: header name
:param length: length of path segment check
:param error_msg: error message for client
:returns: A tuple with path parts according to length
:raise: HTTPPreconditionFailed if header value
is not well formatted.
"""
hdr = unquote(req.headers.get(name))
if not hdr.startswith('/'):
hdr = '/' + hdr
try:
return split_path(hdr, length, length, True)
except ValueError:
raise HTTPPreconditionFailed(
request=req,
body=error_msg)
class SegmentedIterable(object):
"""
Iterable that returns the object contents for a large object.
@ -645,7 +671,7 @@ def resolve_etag_is_at_header(req, metadata):
middleware's alternate etag sysmeta (X-Object-Sysmeta-Crypto-Etag) but will
then find the EC alternate etag (if EC policy). But if the object *is*
encrypted then X-Object-Sysmeta-Crypto-Etag is found and used, which is
correct because it should be preferred over X-Object-Sysmeta-Crypto-Etag.
correct because it should be preferred over X-Object-Sysmeta-Ec-Etag.
:param req: a swob Request
:param metadata: a dict containing object metadata

View File

@ -17,6 +17,7 @@
from __future__ import print_function
import base64
import binascii
import bisect
import errno
@ -29,6 +30,7 @@ import operator
import os
import pwd
import re
import string
import struct
import sys
import time
@ -4591,6 +4593,36 @@ def safe_json_loads(value):
return None
def strict_b64decode(value, allow_line_breaks=False):
'''
Validate and decode Base64-encoded data.
The stdlib base64 module silently discards bad characters, but we often
want to treat them as an error.
:param value: some base64-encoded data
:param allow_line_breaks: if True, ignore carriage returns and newlines
:returns: the decoded data
:raises ValueError: if ``value`` is not a string, contains invalid
characters, or has insufficient padding
'''
if not isinstance(value, six.string_types):
raise ValueError
# b64decode will silently discard bad characters, but we want to
# treat them as an error
valid_chars = string.digits + string.ascii_letters + '/+'
strip_chars = '='
if allow_line_breaks:
valid_chars += '\r\n'
strip_chars += '\r\n'
if any(c not in valid_chars for c in value.strip(strip_chars)):
raise ValueError
try:
return base64.b64decode(value)
except (TypeError, binascii.Error): # (py2 error, py3 error)
raise ValueError
MD5_BLOCK_READ_BYTES = 4096

View File

@ -397,6 +397,24 @@ def loadapp(conf_file, global_conf=None, allow_modify_pipeline=True):
return ctx.create()
def load_app_config(conf_file):
"""
Read the app config section from a config file.
:param conf_file: path to a config file
:return: a dict
"""
app_conf = {}
try:
ctx = loadcontext(loadwsgi.APP, conf_file)
except LookupError:
pass
else:
app_conf.update(ctx.app_context.global_conf)
app_conf.update(ctx.app_context.local_conf)
return app_conf
def run_server(conf, logger, sock, global_conf=None):
# Ensure TZ environment variable exists to avoid stat('/etc/localtime') on
# some platforms. This locks in reported times to UTC.

View File

@ -73,13 +73,17 @@ ic_conf_body = """
# log_statsd_metric_prefix =
[pipeline:main]
pipeline = catch_errors proxy-logging cache proxy-server
pipeline = catch_errors proxy-logging cache symlink proxy-server
[app:proxy-server]
use = egg:swift#proxy
account_autocreate = true
# See proxy-server.conf-sample for options
[filter:symlink]
use = egg:swift#symlink
# See proxy-server.conf-sample for options
[filter:cache]
use = egg:swift#memcache
# See proxy-server.conf-sample for options
@ -568,7 +572,10 @@ class ContainerSync(Daemon):
realm_key, ts_meta):
return True
exc = None
# look up for the newest one
# look up for the newest one; the symlink=get query-string has
# no effect unless symlinks are enabled in the internal client
# in which case it ensures that symlink objects retain their
# symlink property when sync'd.
headers_out = {'X-Newest': True,
'X-Backend-Storage-Policy-Index':
str(info['storage_policy_index'])}
@ -577,7 +584,8 @@ class ContainerSync(Daemon):
self.swift.get_object(info['account'],
info['container'], row['name'],
headers=headers_out,
acceptable_statuses=(2, 4))
acceptable_statuses=(2, 4),
params={'symlink': 'get'})
except (Exception, UnexpectedResponse, Timeout) as err:
headers = {}

View File

@ -2468,7 +2468,12 @@ class BaseDiskFile(object):
:raises DiskFileError: various exceptions from
:func:`swift.obj.diskfile.DiskFile._verify_data_file`
"""
fp = open(data_file, 'rb')
try:
fp = open(data_file, 'rb')
except IOError as e:
if e.errno == errno.ENOENT:
raise DiskFileNotExist()
raise
self._datafile_metadata = self._failsafe_read_metadata(
fp, data_file,
add_missing_checksum=modernize)

View File

@ -794,7 +794,8 @@ class File(Base):
['content_type', 'content-type'],
['last_modified', 'last-modified'],
['etag', 'etag']]
optional_fields = [['x_object_manifest', 'x-object-manifest']]
optional_fields = [['x_object_manifest', 'x-object-manifest'],
['x_symlink_target', 'x-symlink-target']]
header_fields = self.header_fields(fields,
optional_fields=optional_fields)

View File

@ -18,14 +18,13 @@
import unittest2
import json
from uuid import uuid4
from unittest2 import SkipTest
from string import ascii_letters
from six.moves import range
from swift.common.middleware.acl import format_acl
from test.functional import check_response, retry, requires_acls, \
load_constraint
load_constraint, SkipTest
import test.functional as tf

View File

@ -17,11 +17,10 @@
import json
import unittest2
from unittest2 import SkipTest
from uuid import uuid4
from test.functional import check_response, cluster_info, retry, \
requires_acls, load_constraint, requires_policies
requires_acls, load_constraint, requires_policies, SkipTest
import test.functional as tf
from six.moves import range

View File

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import test.functional as tf
from test.functional.tests import Utils, Base, Base2, BaseEnv
from test.functional.swift_test_client import Connection, ResponseError
@ -31,11 +32,6 @@ class TestDloEnv(BaseEnv):
@classmethod
def setUp(cls):
super(TestDloEnv, cls).setUp()
config2 = tf.config.copy()
config2['username'] = tf.config['username3']
config2['password'] = tf.config['password3']
cls.conn2 = Connection(config2)
cls.conn2.authenticate()
cls.container = cls.account.container(Utils.create_name())
cls.container2 = cls.account.container(Utils.create_name())
@ -243,9 +239,15 @@ class TestDlo(Base):
manifest.info(hdrs={'If-None-Match': "not-%s" % etag})
self.assert_status(200)
@unittest.skipIf('username3' not in tf.config, "Requires user 3")
def test_dlo_referer_on_segment_container(self):
# First the account2 (test3) should fail
headers = {'X-Auth-Token': self.env.conn2.storage_token,
config2 = tf.config.copy()
config2['username'] = tf.config['username3']
config2['password'] = tf.config['password3']
conn2 = Connection(config2)
conn2.authenticate()
headers = {'X-Auth-Token': conn2.storage_token,
'Referer': 'http://blah.example.com'}
dlo_file = self.env.container.file("mancont2")
self.assertRaises(ResponseError, dlo_file.read,

View File

@ -18,14 +18,13 @@
import datetime
import json
import unittest2
from unittest2 import SkipTest
from uuid import uuid4
import time
from six.moves import range
from test.functional import check_response, retry, requires_acls, \
requires_policies
requires_policies, SkipTest
import test.functional as tf

View File

@ -19,10 +19,9 @@ import hashlib
import itertools
import json
from copy import deepcopy
from unittest2 import SkipTest
import test.functional as tf
from test.functional import cluster_info
from test.functional import cluster_info, SkipTest
from test.functional.tests import Utils, Base, Base2, BaseEnv
from test.functional.swift_test_client import Connection, ResponseError

1767
test/functional/test_symlink.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -19,14 +19,13 @@ import hashlib
import json
from copy import deepcopy
from six.moves import urllib
from unittest2 import SkipTest
from time import time, strftime, gmtime
import test.functional as tf
from swift.common.middleware import tempurl
from test.functional import cluster_info
from test.functional.tests import Utils, Base, Base2, BaseEnv
from test.functional import requires_acls
from test.functional import requires_acls, SkipTest
from test.functional.swift_test_client import Account, Connection, \
ResponseError

View File

@ -17,12 +17,13 @@
import json
import time
import unittest2
from unittest2 import SkipTest
import test.functional as tf
from copy import deepcopy
from swift.common.utils import MD5_OF_EMPTY_STRING
from test.functional.tests import Base, Base2, BaseEnv, Utils
from test.functional import cluster_info
from test.functional import cluster_info, SkipTest
from test.functional.swift_test_client import Account, Connection, \
ResponseError
@ -207,8 +208,13 @@ class TestObjectVersioning(Base):
try:
# only delete files and not containers
# as they were configured in self.env
# get rid of any versions so they aren't restored
self.env.versions_container.delete_files()
# get rid of originals
self.env.container.delete_files()
# in history mode, deleted originals got copied to versions, so
# clear that again
self.env.versions_container.delete_files()
except ResponseError:
pass
@ -579,6 +585,125 @@ class TestObjectVersioning(Base):
versioned_obj.delete()
self.assertEqual("aaaaa", versioned_obj.read())
def _check_overwriting_symlink(self):
# assertions common to x-versions-location and x-history-location modes
container = self.env.container
versions_container = self.env.versions_container
tgt_a_name = Utils.create_name()
tgt_b_name = Utils.create_name()
tgt_a = container.file(tgt_a_name)
tgt_a.write("aaaaa")
tgt_b = container.file(tgt_b_name)
tgt_b.write("bbbbb")
symlink_name = Utils.create_name()
sym_tgt_header = '%s/%s' % (container.name, tgt_a_name)
sym_headers_a = {'X-Symlink-Target': sym_tgt_header}
symlink = container.file(symlink_name)
symlink.write("", hdrs=sym_headers_a)
self.assertEqual("aaaaa", symlink.read())
sym_headers_b = {'X-Symlink-Target': '%s/%s' % (container.name,
tgt_b_name)}
symlink.write("", hdrs=sym_headers_b)
self.assertEqual("bbbbb", symlink.read())
# the old version got saved off
self.assertEqual(1, versions_container.info()['object_count'])
versioned_obj_name = versions_container.files()[0]
prev_version = versions_container.file(versioned_obj_name)
prev_version_info = prev_version.info(parms={'symlink': 'get'})
self.assertEqual("aaaaa", prev_version.read())
self.assertEqual(MD5_OF_EMPTY_STRING, prev_version_info['etag'])
self.assertEqual(sym_tgt_header,
prev_version_info['x_symlink_target'])
return symlink, tgt_a
def test_overwriting_symlink(self):
if 'symlink' not in cluster_info:
raise SkipTest("Symlinks not enabled")
symlink, target = self._check_overwriting_symlink()
# test delete
symlink.delete()
sym_info = symlink.info(parms={'symlink': 'get'})
self.assertEqual("aaaaa", symlink.read())
self.assertEqual(MD5_OF_EMPTY_STRING, sym_info['etag'])
self.assertEqual('%s/%s' % (self.env.container.name, target.name),
sym_info['x_symlink_target'])
def _setup_symlink(self):
target = self.env.container.file('target-object')
target.write('target object data')
symlink = self.env.container.file('symlink')
symlink.write('', hdrs={
'Content-Type': 'application/symlink',
'X-Symlink-Target': '%s/%s' % (
self.env.container.name, target.name)})
return symlink, target
def _assert_symlink(self, symlink, target):
self.assertEqual('target object data', symlink.read())
self.assertEqual(target.info(), symlink.info())
self.assertEqual('application/symlink',
symlink.info(parms={
'symlink': 'get'})['content_type'])
def _check_copy_destination_restore_symlink(self):
# assertions common to x-versions-location and x-history-location modes
symlink, target = self._setup_symlink()
symlink.write('this is not a symlink')
# the symlink is versioned
version_container_files = self.env.versions_container.files(
parms={'format': 'json'})
self.assertEqual(1, len(version_container_files))
versioned_obj_info = version_container_files[0]
self.assertEqual('application/symlink',
versioned_obj_info['content_type'])
versioned_obj = self.env.versions_container.file(
versioned_obj_info['name'])
# the symlink is still a symlink
self._assert_symlink(versioned_obj, target)
# test manual restore (this creates a new backup of the overwrite)
versioned_obj.copy(self.env.container.name, symlink.name,
parms={'symlink': 'get'})
self._assert_symlink(symlink, target)
# symlink overwritten by write then copy -> 2 versions
self.assertEqual(2, self.env.versions_container.info()['object_count'])
return symlink, target
def test_copy_destination_restore_symlink(self):
if 'symlink' not in cluster_info:
raise SkipTest("Symlinks not enabled")
symlink, target = self._check_copy_destination_restore_symlink()
# and versioned writes restore
symlink.delete()
self.assertEqual(1, self.env.versions_container.info()['object_count'])
self.assertEqual('this is not a symlink', symlink.read())
symlink.delete()
self.assertEqual(0, self.env.versions_container.info()['object_count'])
self._assert_symlink(symlink, target)
def test_put_x_copy_from_restore_symlink(self):
if 'symlink' not in cluster_info:
raise SkipTest("Symlinks not enabled")
symlink, target = self._setup_symlink()
symlink.write('this is not a symlink')
version_container_files = self.env.versions_container.files()
self.assertEqual(1, len(version_container_files))
versioned_obj = self.env.versions_container.file(
version_container_files[0])
symlink.write(parms={'symlink': 'get'}, cfg={
'no_content_type': True}, hdrs={
'X-Copy-From': '%s/%s' % (
self.env.versions_container, versioned_obj.name)})
self._assert_symlink(symlink, target)
class TestObjectVersioningUTF8(Base2, TestObjectVersioning):
@ -692,6 +817,29 @@ class TestObjectVersioningHistoryMode(TestObjectVersioning):
prev_version = self.env.versions_container.file(actual)
self.assertEqual(expected, prev_version.read())
def test_overwriting_symlink(self):
if 'symlink' not in cluster_info:
raise SkipTest("Symlinks not enabled")
symlink, target = self._check_overwriting_symlink()
# test delete
symlink.delete()
with self.assertRaises(ResponseError) as cm:
symlink.read()
self.assertEqual(404, cm.exception.status)
def test_copy_destination_restore_symlink(self):
if 'symlink' not in cluster_info:
raise SkipTest("Symlinks not enabled")
symlink, target = self._check_copy_destination_restore_symlink()
symlink.delete()
with self.assertRaises(ResponseError) as cm:
symlink.read()
self.assertEqual(404, cm.exception.status)
# 2 versions plus delete marker and deleted version
self.assertEqual(4, self.env.versions_container.info()['object_count'])
class TestSloWithVersioning(unittest2.TestCase):

View File

@ -26,7 +26,6 @@ import unittest2
import uuid
from copy import deepcopy
import eventlet
from unittest2 import SkipTest
from swift.common.http import is_success, is_client_error
from email.utils import parsedate
@ -36,7 +35,7 @@ from test.functional import normalized_urls, load_constraint, cluster_info
from test.functional import check_response, retry
import test.functional as tf
from test.functional.swift_test_client import Account, Connection, File, \
ResponseError
ResponseError, SkipTest
def setUpModule():
@ -534,7 +533,7 @@ class TestContainer(Base):
cont = self.env.account.container('a' * l)
if l <= limit:
self.assertTrue(cont.create())
self.assert_status(201)
self.assert_status((201, 202))
else:
self.assertFalse(cont.create())
self.assert_status(400)

View File

@ -27,11 +27,11 @@ from collections import defaultdict
import unittest
from hashlib import md5
from uuid import uuid4
from six.moves.http_client import HTTPConnection
import shutil
from six.moves.http_client import HTTPConnection
from six.moves.urllib.parse import urlparse
from swiftclient import get_auth, head_account
from swiftclient import get_auth, head_account, client
from swift.common import internal_client
from swift.obj.diskfile import get_data_dir
from swift.common.ring import Ring
@ -387,6 +387,10 @@ class ProbeTest(unittest.TestCase):
Manager(['all']).kill()
except Exception:
pass
info_url = "%s://%s/info" % (urlparse(self.url).scheme,
urlparse(self.url).netloc)
proxy_conn = client.http_connection(info_url)
self.cluster_info = client.get_capabilities(proxy_conn)
def tearDown(self):
Manager(['all']).kill()

View File

@ -18,7 +18,6 @@ import uuid
import random
import unittest
from six.moves.urllib.parse import urlparse
from swift.common.manager import Manager
from swift.common.internal_client import InternalClient
from swift.common import utils, direct_client
@ -240,12 +239,7 @@ class TestContainerMergePolicyIndex(ReplProbeTest):
orig_policy_index, node))
def test_reconcile_manifest(self):
info_url = "%s://%s/info" % (urlparse(self.url).scheme,
urlparse(self.url).netloc)
proxy_conn = client.http_connection(info_url)
cluster_info = client.get_capabilities(proxy_conn)
if 'slo' not in cluster_info:
if 'slo' not in self.cluster_info:
raise unittest.SkipTest(
"SLO not enabled in proxy; can't test manifest reconciliation")
# this test is not only testing a split brain scenario on
@ -367,6 +361,80 @@ class TestContainerMergePolicyIndex(ReplProbeTest):
self.assertEqual(int(metadata['content-length']),
sum(part['size_bytes'] for part in manifest_data))
def test_reconcile_symlink(self):
if 'symlink' not in self.cluster_info:
raise unittest.SkipTest(
"Symlink not enabled in proxy; can't test "
"symlink reconciliation")
wrong_policy = random.choice(ENABLED_POLICIES)
policy = random.choice([p for p in ENABLED_POLICIES
if p is not wrong_policy])
# get an old container stashed
self.brain.stop_primary_half()
self.brain.put_container(int(policy))
self.brain.start_primary_half()
# write some target data
client.put_object(self.url, self.token, self.container_name, 'target',
contents='this is the target data')
# write the symlink
self.brain.stop_handoff_half()
self.brain.put_container(int(wrong_policy))
client.put_object(
self.url, self.token, self.container_name, 'symlink',
headers={
'X-Symlink-Target': '%s/target' % self.container_name,
'Content-Type': 'application/symlink',
})
# at this point we have a broken symlink (the container_info has the
# proxy looking for the target in the wrong policy)
with self.assertRaises(ClientException) as ctx:
client.get_object(self.url, self.token, self.container_name,
'symlink')
self.assertEqual(ctx.exception.http_status, 404)
# of course the symlink itself is fine
metadata, body = client.get_object(self.url, self.token,
self.container_name, 'symlink',
query_string='symlink=get')
self.assertEqual(metadata['x-symlink-target'],
'%s/target' % self.container_name)
self.assertEqual(metadata['content-type'], 'application/symlink')
self.assertEqual(body, '')
# ... although in the wrong policy
object_ring = POLICIES.get_object_ring(int(wrong_policy), '/etc/swift')
part, nodes = object_ring.get_nodes(
self.account, self.container_name, 'symlink')
for node in nodes:
metadata = direct_client.direct_head_object(
node, part, self.account, self.container_name, 'symlink',
headers={'X-Backend-Storage-Policy-Index': int(wrong_policy)})
self.assertEqual(metadata['X-Object-Sysmeta-Symlink-Target'],
'%s/target' % self.container_name)
# let the reconciler run
self.brain.start_handoff_half()
self.get_to_final_state()
Manager(['container-reconciler']).once()
# clear proxy cache
client.post_container(self.url, self.token, self.container_name, {})
# now the symlink works
metadata, body = client.get_object(self.url, self.token,
self.container_name, 'symlink')
self.assertEqual(body, 'this is the target data')
# and it's in the correct policy
object_ring = POLICIES.get_object_ring(int(policy), '/etc/swift')
part, nodes = object_ring.get_nodes(
self.account, self.container_name, 'symlink')
for node in nodes:
metadata = direct_client.direct_head_object(
node, part, self.account, self.container_name, 'symlink',
headers={'X-Backend-Storage-Policy-Index': int(policy)})
self.assertEqual(metadata['X-Object-Sysmeta-Symlink-Target'],
'%s/target' % self.container_name)
def test_reconciler_move_object_twice(self):
# select some policies
old_policy = random.choice(ENABLED_POLICIES)

View File

@ -25,14 +25,17 @@ from test.probe.brain import BrainSplitter
from test.probe.common import ReplProbeTest, ENABLED_POLICIES
def get_current_realm_cluster(url):
def get_info(url):
parts = urlparse(url)
url = parts.scheme + '://' + parts.netloc + '/info'
http_conn = client.http_connection(url)
try:
info = client.get_capabilities(http_conn)
return client.get_capabilities(http_conn)
except client.ClientException:
raise unittest.SkipTest('Unable to retrieve cluster info')
def get_current_realm_cluster(info):
try:
realms = info['container_sync']['realms']
except KeyError:
@ -44,11 +47,12 @@ def get_current_realm_cluster(url):
raise unittest.SkipTest('Unable find current realm cluster')
class TestContainerSync(ReplProbeTest):
class BaseTestContainerSync(ReplProbeTest):
def setUp(self):
super(TestContainerSync, self).setUp()
self.realm, self.cluster = get_current_realm_cluster(self.url)
super(BaseTestContainerSync, self).setUp()
self.info = get_info(self.url)
self.realm, self.cluster = get_current_realm_cluster(self.info)
def _setup_synced_containers(
self, source_overrides=None, dest_overrides=None):
@ -92,6 +96,9 @@ class TestContainerSync(ReplProbeTest):
return source['name'], dest['name']
class TestContainerSync(BaseTestContainerSync):
def test_sync(self):
source_container, dest_container = self._setup_synced_containers()
@ -377,5 +384,184 @@ class TestContainerSync(ReplProbeTest):
self.assertEqual(body, 'new-test-body')
class TestContainerSyncAndSymlink(BaseTestContainerSync):
def setUp(self):
super(TestContainerSyncAndSymlink, self).setUp()
symlinks_enabled = self.info.get('symlink') or False
if not symlinks_enabled:
raise unittest.SkipTest("Symlinks not enabled")
def test_sync_symlink(self):
# Verify that symlinks are sync'd as symlinks.
dest_account = self.account_2
source_container, dest_container = self._setup_synced_containers(
dest_overrides=dest_account
)
# Create source and dest containers for target objects in separate
# accounts.
# These containers must have same name for the destination symlink
# to use the same target object. Initially the destination has no sync
# key so target will not sync.
tgt_container = 'targets-%s' % uuid.uuid4()
dest_tgt_info = dict(dest_account)
dest_tgt_info.update({'name': tgt_container, 'sync_key': None})
self._setup_synced_containers(
source_overrides={'name': tgt_container, 'sync_key': 'tgt_key'},
dest_overrides=dest_tgt_info)
# upload a target to source
target_name = 'target-%s' % uuid.uuid4()
target_body = 'target body'
client.put_object(
self.url, self.token, tgt_container, target_name,
target_body)
# Note that this tests when the target object is in the same account
target_path = '%s/%s' % (tgt_container, target_name)
symlink_name = 'symlink-%s' % uuid.uuid4()
put_headers = {'X-Symlink-Target': target_path}
# upload the symlink
client.put_object(
self.url, self.token, source_container, symlink_name,
'', headers=put_headers)
# verify object is a symlink
resp_headers, symlink_body = client.get_object(
self.url, self.token, source_container, symlink_name,
query_string='symlink=get')
self.assertEqual('', symlink_body)
self.assertIn('x-symlink-target', resp_headers)
# verify symlink behavior
resp_headers, actual_target_body = client.get_object(
self.url, self.token, source_container, symlink_name)
self.assertEqual(target_body, actual_target_body)
# cycle container-sync
Manager(['container-sync']).once()
# verify symlink was sync'd
resp_headers, dest_listing = client.get_container(
dest_account['url'], dest_account['token'], dest_container)
self.assertFalse(dest_listing[1:])
self.assertEqual(symlink_name, dest_listing[0]['name'])
# verify symlink remained only a symlink
resp_headers, symlink_body = client.get_object(
dest_account['url'], dest_account['token'], dest_container,
symlink_name, query_string='symlink=get')
self.assertEqual('', symlink_body)
self.assertIn('x-symlink-target', resp_headers)
# attempt to GET the target object via symlink will fail because
# the target wasn't sync'd
with self.assertRaises(ClientException) as cm:
client.get_object(dest_account['url'], dest_account['token'],
dest_container, symlink_name)
self.assertEqual(404, cm.exception.http_status)
# now set sync key on destination target container
client.put_container(
dest_account['url'], dest_account['token'], tgt_container,
headers={'X-Container-Sync-Key': 'tgt_key'})
# cycle container-sync
Manager(['container-sync']).once()
# sanity:
resp_headers, body = client.get_object(
dest_account['url'], dest_account['token'],
tgt_container, target_name)
# sanity check - verify symlink remained only a symlink
resp_headers, symlink_body = client.get_object(
dest_account['url'], dest_account['token'], dest_container,
symlink_name, query_string='symlink=get')
self.assertEqual('', symlink_body)
self.assertIn('x-symlink-target', resp_headers)
# verify GET of target object via symlink now succeeds
resp_headers, actual_target_body = client.get_object(
dest_account['url'], dest_account['token'], dest_container,
symlink_name)
self.assertEqual(target_body, actual_target_body)
def test_sync_cross_acc_symlink(self):
# Verify that cross-account symlinks are sync'd as cross-account
# symlinks.
source_container, dest_container = self._setup_synced_containers()
# Sync'd symlinks will have the same target path "/a/c/o".
# So if we want to execute probe test with syncing targets,
# two swift clusters will be required.
# Therefore, for probe test in single cluster, target object is not
# sync'd in this test.
tgt_account = self.account_2
tgt_container = 'targets-%s' % uuid.uuid4()
tgt_container_headers = {'X-Container-Read': 'test:tester'}
if len(ENABLED_POLICIES) > 1:
tgt_policy = random.choice(ENABLED_POLICIES)
tgt_container_headers['X-Storage-Policy'] = tgt_policy.name
client.put_container(tgt_account['url'], tgt_account['token'],
tgt_container, headers=tgt_container_headers)
# upload a target to source
target_name = 'target-%s' % uuid.uuid4()
target_body = 'target body'
client.put_object(tgt_account['url'], tgt_account['token'],
tgt_container, target_name, target_body)
# Note that this tests when the target object is in a different account
target_path = '%s/%s' % (tgt_container, target_name)
symlink_name = 'symlink-%s' % uuid.uuid4()
put_headers = {
'X-Symlink-Target': target_path,
'X-Symlink-Target-Account': tgt_account['account']}
# upload the symlink
client.put_object(
self.url, self.token, source_container, symlink_name,
'', headers=put_headers)
# verify object is a cross-account symlink
resp_headers, symlink_body = client.get_object(
self.url, self.token, source_container, symlink_name,
query_string='symlink=get')
self.assertEqual('', symlink_body)
self.assertIn('x-symlink-target', resp_headers)
self.assertIn('x-symlink-target-account', resp_headers)
# verify symlink behavior
resp_headers, actual_target_body = client.get_object(
self.url, self.token, source_container, symlink_name)
self.assertEqual(target_body, actual_target_body)
# cycle container-sync
Manager(['container-sync']).once()
# verify symlink was sync'd
resp_headers, dest_listing = client.get_container(
self.url, self.token, dest_container)
self.assertFalse(dest_listing[1:])
self.assertEqual(symlink_name, dest_listing[0]['name'])
# verify symlink remained only a symlink
resp_headers, symlink_body = client.get_object(
self.url, self.token, dest_container,
symlink_name, query_string='symlink=get')
self.assertEqual('', symlink_body)
self.assertIn('x-symlink-target', resp_headers)
self.assertIn('x-symlink-target-account', resp_headers)
# verify GET of target object via symlink now succeeds
resp_headers, actual_target_body = client.get_object(
self.url, self.token, dest_container, symlink_name)
self.assertEqual(target_body, actual_target_body)
if __name__ == "__main__":
unittest.main()

View File

@ -1092,6 +1092,37 @@ class TestReconCommands(unittest.TestCase):
# that is returned from the recon middleware, thus can't rely on it
mock_print.assert_has_calls(default_calls, any_order=True)
@mock.patch('six.moves.builtins.print')
def test_version_check(self, mock_print):
version = "2.7.1.dev144"
def dummy_request(*args, **kwargs):
return [
('http://127.0.0.1:6010/recon/version',
{'version': version},
200,
0,
0),
('http://127.0.0.1:6020/recon/version',
{'version': version},
200,
0,
0),
]
cli = recon.SwiftRecon()
cli.pool.imap = dummy_request
default_calls = [
mock.call("Versions matched (%s), "
"0 error[s] while checking hosts." % version)
]
cli.version_check([('127.0.0.1', 6010), ('127.0.0.1', 6020)])
# We need any_order=True because the order of calls depends on the dict
# that is returned from the recon middleware, thus can't rely on it
mock_print.assert_has_calls(default_calls, any_order=True)
@mock.patch('six.moves.builtins.print')
@mock.patch('time.time')
def test_time_check_jitter_mismatch(self, mock_now, mock_print):
@ -1126,7 +1157,35 @@ class TestReconCommands(unittest.TestCase):
]
cli.time_check([('127.0.0.1', 6010), ('127.0.0.1', 6020)], 3)
# We need any_order=True because the order of calls depends on the dict
# that is returned from the recon middleware, thus can't rely on it
mock_print.assert_has_calls(default_calls, any_order=True)
@mock.patch('six.moves.builtins.print')
def test_version_check_differs(self, mock_print):
def dummy_request(*args, **kwargs):
return [
('http://127.0.0.1:6010/recon/version',
{'version': "2.7.1.dev144"},
200,
0,
0),
('http://127.0.0.1:6020/recon/version',
{'version': "2.7.1.dev145"},
200,
0,
0),
]
cli = recon.SwiftRecon()
cli.pool.imap = dummy_request
default_calls = [
mock.call("Versions not matched (2.7.1.dev144, 2.7.1.dev145), "
"0 error[s] while checking hosts.")
]
cli.version_check([('127.0.0.1', 6010), ('127.0.0.1', 6020)])
# We need any_order=True because the order of calls depends on the dict
# that is returned from the recon middleware, thus can't rely on it
mock_print.assert_has_calls(default_calls, any_order=True)

View File

@ -46,7 +46,6 @@ class RunSwiftRingBuilderMixin(object):
if 'exp_results' in kwargs:
exp_results = kwargs['exp_results']
argv = argv[:-1]
else:
exp_results = None
@ -1917,7 +1916,102 @@ class TestCommands(unittest.TestCase, RunSwiftRingBuilderMixin):
ring.save(self.tmpfile)
# Test No change to the device
argv = ["", self.tmpfile, "rebalance", "3"]
with mock.patch('swift.common.ring.RingBuilder.save') as mock_save:
self.assertSystemExit(EXIT_WARNING, ringbuilder.main, argv)
self.assertEqual(len(mock_save.calls), 0)
def test_rebalance_saves_dispersion_improvement(self):
# We set up a situation where dispersion improves but balance
# doesn't. We construct a ring with one zone, then add a second zone
# concurrently with a new device in the first zone. That first
# device won't acquire any partitions, so the ring's balance won't
# change. However, dispersion will improve.
ring = RingBuilder(6, 5, 1)
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sda'})
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sdb'})
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sdc'})
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sdd'})
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sde'})
ring.rebalance()
# The last guy in zone 1
ring.add_dev({
'region': 1, 'zone': 1,
'ip': '10.0.0.1', 'port': 20001, 'weight': 1000,
'device': 'sdf'})
# Add zone 2 (same total weight as zone 1)
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sda'})
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sdb'})
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sdc'})
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sdd'})
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sde'})
ring.add_dev({
'region': 1, 'zone': 2,
'ip': '10.0.0.2', 'port': 20001, 'weight': 1000,
'device': 'sdf'})
ring.pretend_min_part_hours_passed()
ring.save(self.tmpfile)
del ring
# Rebalance once: this gets 1/5 replica into zone 2; the ring is
# saved because devices changed.
argv = ["", self.tmpfile, "rebalance", "5759339"]
self.assertSystemExit(EXIT_WARNING, ringbuilder.main, argv)
rb = RingBuilder.load(self.tmpfile)
self.assertEqual(rb.dispersion, 100)
self.assertEqual(rb.get_balance(), 100)
self.run_srb('pretend_min_part_hours_passed')
# Rebalance again: this gets 2/5 replica into zone 2, but no devices
# changed and the balance stays the same. The only improvement is
# dispersion.
captured = {}
def capture_save(rb, path):
captured['dispersion'] = rb.dispersion
captured['balance'] = rb.get_balance()
# The warning is benign; it's just telling the user to keep on
# rebalancing. The important assertion is that the builder was
# saved.
with mock.patch('swift.common.ring.RingBuilder.save', capture_save):
self.assertSystemExit(EXIT_WARNING, ringbuilder.main, argv)
self.assertEqual(captured, {
'dispersion': 0,
'balance': 100,
})
def test_rebalance_no_devices(self):
# Test no devices
@ -2078,6 +2172,13 @@ class TestCommands(unittest.TestCase, RunSwiftRingBuilderMixin):
argv = ["", self.tmpfile, "write_ring"]
self.assertSystemExit(EXIT_SUCCESS, ringbuilder.main, argv)
def test_write_empty_ring(self):
ring = RingBuilder(6, 3, 1)
ring.save(self.tmpfile)
exp_results = {'valid_exit_codes': [2]}
out, err = self.run_srb("write_ring", exp_results=exp_results)
self.assertEqual('Unable to write empty ring.\n', out)
def test_write_builder(self):
# Test builder file already exists
self.create_sample_ring()

View File

@ -19,6 +19,7 @@ import unittest
import mock
from swift.common.utils import MD5_OF_EMPTY_STRING
from swift.common.header_key_dict import HeaderKeyDict
from swift.common.middleware.crypto import decrypter
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK, \
@ -960,6 +961,27 @@ class TestDecrypterContainerRequests(unittest.TestCase):
self.assertIn("Cipher must be AES_CTR_256",
self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_container_json_not_encrypted_obj(self):
pt_etag = '%s; symlink_path=/a/c/o' % MD5_OF_EMPTY_STRING
obj_dict = {"bytes": 0,
"last_modified": "2015-04-14T23:33:06.439040",
"hash": pt_etag,
"name": "symlink",
"content_type": 'application/symlink'}
listing = [obj_dict]
fake_body = json.dumps(listing)
resp = self._make_cont_get_req(fake_body, 'json')
self.assertEqual('200 OK', resp.status)
body = resp.body
self.assertEqual(len(body), int(resp.headers['Content-Length']))
body_json = json.loads(body)
self.assertEqual(1, len(body_json))
self.assertEqual(pt_etag, body_json[0]['hash'])
class TestModuleMethods(unittest.TestCase):
def test_purge_crypto_sysmeta_headers(self):

View File

@ -1309,6 +1309,9 @@ class TestServerSideCopyConfiguration(unittest.TestCase):
[pipeline:main]
pipeline = catch_errors copy ye-olde-proxy-server
[filter:catch_errors]
use = egg:swift#catch_errors
[filter:copy]
use = egg:swift#copy

View File

@ -811,6 +811,9 @@ class TestDloConfiguration(unittest.TestCase):
[pipeline:main]
pipeline = catch_errors dlo ye-olde-proxy-server
[filter:catch_errors]
use = egg:swift#catch_errors
[filter:dlo]
use = egg:swift#dlo
max_get_time = 3600
@ -845,13 +848,16 @@ class TestDloConfiguration(unittest.TestCase):
[pipeline:main]
pipeline = catch_errors dlo ye-olde-proxy-server
[filter:catch_errors]
use = egg:swift#catch_errors
[filter:dlo]
use = egg:swift#dlo
[app:ye-olde-proxy-server]
use = egg:swift#proxy
rate_limit_after_segment = 13
max_get_time = 2900
set max_get_time = 2900
""")
conffile = tempfile.NamedTemporaryFile()
@ -878,6 +884,9 @@ class TestDloConfiguration(unittest.TestCase):
""")
proxy_conf2 = dedent("""
[filter:catch_errors]
use = egg:swift#catch_errors
[filter:dlo]
use = egg:swift#dlo

View File

@ -0,0 +1,929 @@
#!/usr/bin/env python
# Copyright (c) 2016 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import json
import mock
from six.moves.urllib.parse import quote, parse_qs
from swift.common import swob
from swift.common.middleware import symlink, copy, versioned_writes, \
listing_formats
from swift.common.swob import Request
from swift.common.utils import MD5_OF_EMPTY_STRING
from test.unit.common.middleware.helpers import FakeSwift
from test.unit.common.middleware.test_versioned_writes import FakeCache
class TestSymlinkMiddlewareBase(unittest.TestCase):
def setUp(self):
self.app = FakeSwift()
self.sym = symlink.filter_factory({
'symloop_max': '2',
})(self.app)
self.sym.logger = self.app.logger
def call_app(self, req, app=None, expect_exception=False):
if app is None:
app = self.app
self.authorized = []
def authorize(req):
self.authorized.append(req)
if 'swift.authorize' not in req.environ:
req.environ['swift.authorize'] = authorize
status = [None]
headers = [None]
def start_response(s, h, ei=None):
status[0] = s
headers[0] = h
body_iter = app(req.environ, start_response)
body = ''
caught_exc = None
try:
for chunk in body_iter:
body += chunk
except Exception as exc:
if expect_exception:
caught_exc = exc
else:
raise
if expect_exception:
return status[0], headers[0], body, caught_exc
else:
return status[0], headers[0], body
def call_sym(self, req, **kwargs):
return self.call_app(req, app=self.sym, **kwargs)
class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
def test_symlink_simple_put(self):
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
req = Request.blank('/v1/a/c/symlink', method='PUT',
headers={'X-Symlink-Target': 'c1/o'},
body='')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '201 Created')
method, path, hdrs = self.app.calls_with_headers[0]
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
self.assertEqual(val, 'c1/o')
self.assertNotIn('X-Object-Sysmeta-Symlink-Target-Account', hdrs)
val = hdrs.get('X-Object-Sysmeta-Container-Update-Override-Etag')
self.assertEqual(val, '%s; symlink_target=c1/o' % MD5_OF_EMPTY_STRING)
def test_symlink_put_different_account(self):
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
req = Request.blank('/v1/a/c/symlink', method='PUT',
headers={'X-Symlink-Target': 'c1/o',
'X-Symlink-Target-Account': 'a1'},
body='')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '201 Created')
method, path, hdrs = self.app.calls_with_headers[0]
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
self.assertEqual(val, 'c1/o')
self.assertEqual(hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'),
'a1')
def test_symlink_put_leading_slash(self):
self.app.register('PUT', '/v1/a/c/symlink', swob.HTTPCreated, {})
req = Request.blank('/v1/a/c/symlink', method='PUT',
headers={'X-Symlink-Target': '/c1/o'},
body='')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '412 Precondition Failed')
self.assertEqual(body, "X-Symlink-Target header must be of "
"the form <container name>/<object name>")
def test_symlink_put_non_zero_length(self):
req = Request.blank('/v1/a/c/symlink', method='PUT', body='req_body',
headers={'X-Symlink-Target': 'c1/o'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '400 Bad Request')
self.assertEqual(body, 'Symlink requests require a zero byte body')
def test_symlink_put_bad_object_header(self):
req = Request.blank('/v1/a/c/symlink', method='PUT',
headers={'X-Symlink-Target': 'o'},
body='')
status, headers, body = self.call_sym(req)
self.assertEqual(status, "412 Precondition Failed")
self.assertEqual(body, "X-Symlink-Target header must be of "
"the form <container name>/<object name>")
def test_symlink_put_bad_account_header(self):
req = Request.blank('/v1/a/c/symlink', method='PUT',
headers={'X-Symlink-Target': 'c1/o',
'X-Symlink-Target-Account': 'a1/c1'},
body='')
status, headers, body = self.call_sym(req)
self.assertEqual(status, "412 Precondition Failed")
self.assertEqual(body, "Account name cannot contain slashes")
def test_get_symlink(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o'})
req = Request.blank('/v1/a/c/symlink?symlink=get', method='GET')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertIn(('X-Symlink-Target', 'c1/o'), headers)
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
def test_get_symlink_with_account(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
req = Request.blank('/v1/a/c/symlink?symlink=get', method='GET')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertIn(('X-Symlink-Target', 'c1/o'), headers)
self.assertIn(('X-Symlink-Target-Account', 'a2'), headers)
def test_get_symlink_not_found(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPNotFound, {})
req = Request.blank('/v1/a/c/symlink', method='GET')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '404 Not Found')
self.assertNotIn('Content-Location', dict(headers))
def test_get_target_object(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('GET', '/v1/a2/c1/o', swob.HTTPOk, {}, 'resp_body')
req_headers = {'X-Newest': 'True', 'X-Backend-Something': 'future'}
req = Request.blank('/v1/a/c/symlink', method='GET',
headers=req_headers)
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertEqual(body, 'resp_body')
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertIn(('Content-Location', '/v1/a2/c1/o'), headers)
calls = self.app.calls_with_headers
req_headers['Host'] = 'localhost:80'
self.assertEqual(req_headers, calls[0].headers)
req_headers['User-Agent'] = 'Swift'
self.assertEqual(req_headers, calls[1].headers)
self.assertFalse(calls[2:])
def test_get_target_object_not_found(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-account': 'a2'})
self.app.register('GET', '/v1/a2/c1/o', swob.HTTPNotFound, {}, '')
req = Request.blank('/v1/a/c/symlink', method='GET')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '404 Not Found')
self.assertEqual(body, '')
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertIn(('Content-Location', '/v1/a2/c1/o'), headers)
def test_get_target_object_range_not_satisfiable(self):
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('GET', '/v1/a2/c1/o',
swob.HTTPRequestedRangeNotSatisfiable, {}, '')
req = Request.blank('/v1/a/c/symlink', method='GET',
headers={'Range': 'bytes=1-2'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '416 Requested Range Not Satisfiable')
self.assertEqual(
body, '<html><h1>Requested Range Not Satisfiable</h1>'
'<p>The Range requested is not available.</p></html>')
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertIn(('Content-Location', '/v1/a2/c1/o'), headers)
def test_get_ec_symlink_range_unsatisfiable_can_redirect_to_target(self):
self.app.register('GET', '/v1/a/c/symlink',
swob.HTTPRequestedRangeNotSatisfiable,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('GET', '/v1/a2/c1/o', swob.HTTPOk,
{'Content-Range': 'bytes 1-2/10'}, 'es')
req = Request.blank('/v1/a/c/symlink', method='GET',
headers={'Range': 'bytes=1-2'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertEqual(body, 'es')
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertIn(('Content-Location', '/v1/a2/c1/o'), headers)
self.assertIn(('Content-Range', 'bytes 1-2/10'), headers)
def test_get_non_symlink(self):
# this is not symlink object
self.app.register('GET', '/v1/a/c/obj', swob.HTTPOk, {}, 'resp_body')
req = Request.blank('/v1/a/c/obj', method='GET')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertEqual(body, 'resp_body')
# Assert special headers for symlink are not in response
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertNotIn('Content-Location', dict(headers))
def test_head_symlink(self):
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Meta-Color': 'Red'})
req = Request.blank('/v1/a/c/symlink?symlink=get', method='HEAD')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertIn(('X-Symlink-Target', 'c1/o'), headers)
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertIn(('X-Object-Meta-Color', 'Red'), headers)
def test_head_symlink_with_account(self):
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2',
'X-Object-Meta-Color': 'Red'})
req = Request.blank('/v1/a/c/symlink?symlink=get', method='HEAD')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertIn(('X-Symlink-Target', 'c1/o'), headers)
self.assertIn(('X-Symlink-Target-Account', 'a2'), headers)
self.assertIn(('X-Object-Meta-Color', 'Red'), headers)
def test_head_target_object(self):
# this test is also validating that the symlink metadata is not
# returned, but the target object metadata does return
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2',
'X-Object-Meta-Color': 'Red'})
self.app.register('HEAD', '/v1/a2/c1/o', swob.HTTPOk,
{'X-Object-Meta-Color': 'Green'})
req_headers = {'X-Newest': 'True', 'X-Backend-Something': 'future'}
req = Request.blank('/v1/a/c/symlink', method='HEAD',
headers=req_headers)
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertNotIn('X-Symlink-Target', dict(headers))
self.assertNotIn('X-Symlink-Target-Account', dict(headers))
self.assertNotIn(('X-Object-Meta-Color', 'Red'), headers)
self.assertIn(('X-Object-Meta-Color', 'Green'), headers)
self.assertIn(('Content-Location', '/v1/a2/c1/o'), headers)
calls = self.app.calls_with_headers
req_headers['Host'] = 'localhost:80'
self.assertEqual(req_headers, calls[0].headers)
req_headers['User-Agent'] = 'Swift'
self.assertEqual(req_headers, calls[1].headers)
self.assertFalse(calls[2:])
def test_symlink_too_deep(self):
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
self.app.register('HEAD', '/v1/a/c/sym1', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/sym2'})
self.app.register('HEAD', '/v1/a/c/sym2', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/o'})
req = Request.blank('/v1/a/c/symlink', method='HEAD')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '409 Conflict')
def test_symlink_change_symloopmax(self):
# similar test to test_symlink_too_deep, but now changed the limit to 3
self.sym = symlink.filter_factory({
'symloop_max': '3',
})(self.app)
self.sym.logger = self.app.logger
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
self.app.register('HEAD', '/v1/a/c/sym1', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/sym2'})
self.app.register('HEAD', '/v1/a/c/sym2', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/o',
'X-Object-Meta-Color': 'Red'})
self.app.register('HEAD', '/v1/a/c/o', swob.HTTPOk,
{'X-Object-Meta-Color': 'Green'})
req = Request.blank('/v1/a/c/symlink', method='HEAD')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
# assert that the correct metadata was returned
self.assertNotIn(('X-Object-Meta-Color', 'Red'), headers)
self.assertIn(('X-Object-Meta-Color', 'Green'), headers)
def test_sym_to_sym_to_target(self):
# this test is also validating that the symlink metadata is not
# returned, but the target object metadata does return
self.app.register('HEAD', '/v1/a/c/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1',
'X-Object-Meta-Color': 'Red'})
self.app.register('HEAD', '/v1/a/c/sym1', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Meta-Color': 'Yellow'})
self.app.register('HEAD', '/v1/a/c1/o', swob.HTTPOk,
{'X-Object-Meta-Color': 'Green'})
req = Request.blank('/v1/a/c/symlink', method='HEAD')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
self.assertNotIn(('X-Symlink-Target', 'c1/o'), headers)
self.assertNotIn(('X-Symlink-Target-Account', 'a2'), headers)
self.assertNotIn(('X-Object-Meta-Color', 'Red'), headers)
self.assertNotIn(('X-Object-Meta-Color', 'Yellow'), headers)
self.assertIn(('X-Object-Meta-Color', 'Green'), headers)
self.assertIn(('Content-Location', '/v1/a/c1/o'), headers)
def test_symlink_post(self):
self.app.register('POST', '/v1/a/c/symlink', swob.HTTPAccepted,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o'})
req = Request.blank('/v1/a/c/symlink', method='POST',
headers={'X-Object-Meta-Color': 'Red'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '307 Temporary Redirect')
self.assertEqual(body,
'The requested POST was applied to a symlink. POST '
'directly to the target to apply requested metadata.')
method, path, hdrs = self.app.calls_with_headers[0]
val = hdrs.get('X-Object-Meta-Color')
self.assertEqual(val, 'Red')
def test_non_symlink_post(self):
self.app.register('POST', '/v1/a/c/o', swob.HTTPAccepted, {})
req = Request.blank('/v1/a/c/o', method='POST',
headers={'X-Object-Meta-Color': 'Red'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '202 Accepted')
def test_set_symlink_POST_fail(self):
# Setting a link with a POST request is not allowed
req = Request.blank('/v1/a/c/o', method='POST',
headers={'X-Symlink-Target': 'c1/regular_obj'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '400 Bad Request')
self.assertEqual(body, "A PUT request is required to set a symlink "
"target")
def test_symlink_post_but_fail_at_server(self):
self.app.register('POST', '/v1/a/c/o', swob.HTTPNotFound, {})
req = Request.blank('/v1/a/c/o', method='POST',
headers={'X-Object-Meta-Color': 'Red'})
status, headers, body = self.call_sym(req)
self.assertEqual(status, '404 Not Found')
def test_check_symlink_header(self):
def do_test(headers):
req = Request.blank('/v1/a/c/o', method='PUT',
headers=headers)
symlink._check_symlink_header(req)
# normal cases
do_test({'X-Symlink-Target': 'c1/o1'})
do_test({'X-Symlink-Target': 'c1/sub/o1'})
do_test({'X-Symlink-Target': 'c1%2Fo1'})
# specify account
do_test({'X-Symlink-Target': 'c1/o1',
'X-Symlink-Target-Account': 'another'})
# URL encoded is safe
do_test({'X-Symlink-Target': 'c1%2Fo1'})
# URL encoded + multibytes is also safe
do_test(
{'X-Symlink-Target':
u'\u30b0\u30e9\u30d6\u30eb/\u30a2\u30ba\u30ec\u30f3'})
target = u'\u30b0\u30e9\u30d6\u30eb/\u30a2\u30ba\u30ec\u30f3'
encoded_target = quote(target.encode('utf-8'), '')
do_test({'X-Symlink-Target': encoded_target})
do_test(
{'X-Symlink-Target': 'cont/obj',
'X-Symlink-Target-Account': u'\u30b0\u30e9\u30d6\u30eb'})
def test_check_symlink_header_invalid_format(self):
def do_test(headers, status, err_msg):
req = Request.blank('/v1/a/c/o', method='PUT',
headers=headers)
with self.assertRaises(swob.HTTPException) as cm:
symlink._check_symlink_header(req)
self.assertEqual(cm.exception.status, status)
self.assertEqual(cm.exception.body, err_msg)
do_test({'X-Symlink-Target': '/c1/o1'},
'412 Precondition Failed',
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
do_test({'X-Symlink-Target': 'c1o1'},
'412 Precondition Failed',
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
do_test({'X-Symlink-Target': 'c1/o1',
'X-Symlink-Target-Account': '/another'},
'412 Precondition Failed',
'Account name cannot contain slashes')
do_test({'X-Symlink-Target': 'c1/o1',
'X-Symlink-Target-Account': 'an/other'},
'412 Precondition Failed',
'Account name cannot contain slashes')
# url encoded case
do_test({'X-Symlink-Target': '%2Fc1%2Fo1'},
'412 Precondition Failed',
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
do_test({'X-Symlink-Target': 'c1/o1',
'X-Symlink-Target-Account': '%2Fanother'},
'412 Precondition Failed',
'Account name cannot contain slashes')
do_test({'X-Symlink-Target': 'c1/o1',
'X-Symlink-Target-Account': 'an%2Fother'},
'412 Precondition Failed',
'Account name cannot contain slashes')
# with multi-bytes
do_test(
{'X-Symlink-Target':
u'/\u30b0\u30e9\u30d6\u30eb/\u30a2\u30ba\u30ec\u30f3'},
'412 Precondition Failed',
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
target = u'/\u30b0\u30e9\u30d6\u30eb/\u30a2\u30ba\u30ec\u30f3'
encoded_target = quote(target.encode('utf-8'), '')
do_test(
{'X-Symlink-Target': encoded_target},
'412 Precondition Failed',
'X-Symlink-Target header must be of the '
'form <container name>/<object name>')
account = u'\u30b0\u30e9\u30d6\u30eb/\u30a2\u30ba\u30ec\u30f3'
encoded_account = quote(account.encode('utf-8'), '')
do_test(
{'X-Symlink-Target': 'c/o',
'X-Symlink-Target-Account': encoded_account},
'412 Precondition Failed',
'Account name cannot contain slashes')
def test_check_symlink_header_points_to_itself(self):
req = Request.blank('/v1/a/c/o', method='PUT',
headers={'X-Symlink-Target': 'c/o'})
with self.assertRaises(swob.HTTPException) as cm:
symlink._check_symlink_header(req)
self.assertEqual(cm.exception.status, '400 Bad Request')
self.assertEqual(cm.exception.body, 'Symlink cannot target itself')
# Even if set account to itself, it will fail as well
req = Request.blank('/v1/a/c/o', method='PUT',
headers={'X-Symlink-Target': 'c/o',
'X-Symlink-Target-Account': 'a'})
with self.assertRaises(swob.HTTPException) as cm:
symlink._check_symlink_header(req)
self.assertEqual(cm.exception.status, '400 Bad Request')
self.assertEqual(cm.exception.body, 'Symlink cannot target itself')
# sanity, the case to another account is safe
req = Request.blank('/v1/a/c/o', method='PUT',
headers={'X-Symlink-Target': 'c/o',
'X-Symlink-Target-Account': 'a1'})
symlink._check_symlink_header(req)
def test_symloop_max_config(self):
self.app = FakeSwift()
# sanity
self.sym = symlink.filter_factory({
'symloop_max': '1',
})(self.app)
self.assertEqual(self.sym.symloop_max, 1)
# < 1 case will result in default
self.sym = symlink.filter_factory({
'symloop_max': '-1',
})(self.app)
self.assertEqual(self.sym.symloop_max, symlink.DEFAULT_SYMLOOP_MAX)
class SymlinkCopyingTestCase(TestSymlinkMiddlewareBase):
# verify interaction of copy and symlink middlewares
def setUp(self):
self.app = FakeSwift()
conf = {'symloop_max': '2'}
self.sym = symlink.filter_factory(conf)(self.app)
self.sym.logger = self.app.logger
self.copy = copy.filter_factory({})(self.sym)
def call_copy(self, req, **kwargs):
return self.call_app(req, app=self.copy, **kwargs)
def test_copy_symlink_target(self):
req = Request.blank('/v1/a/src_cont/symlink', method='COPY',
headers={'Destination': 'tgt_cont/tgt_obj'})
self._test_copy_symlink_target(req)
req = Request.blank('/v1/a/tgt_cont/tgt_obj', method='PUT',
headers={'X-Copy-From': 'src_cont/symlink'})
self._test_copy_symlink_target(req)
def _test_copy_symlink_target(self, req):
self.app.register('GET', '/v1/a/src_cont/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('GET', '/v1/a2/c1/o', swob.HTTPOk, {}, 'resp_body')
self.app.register('PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated,
{}, 'resp_body')
status, headers, body = self.call_copy(req)
method, path, hdrs = self.app.calls_with_headers[0]
self.assertEqual(method, 'GET')
self.assertEqual(path, '/v1/a/src_cont/symlink')
self.assertEqual('/src_cont/symlink', hdrs.get('X-Copy-From'))
method, path, hdrs = self.app.calls_with_headers[1]
self.assertEqual(method, 'GET')
self.assertEqual(path, '/v1/a2/c1/o')
self.assertEqual('/src_cont/symlink', hdrs.get('X-Copy-From'))
method, path, hdrs = self.app.calls_with_headers[2]
self.assertEqual(method, 'PUT')
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
# this is raw object copy
self.assertEqual(val, None)
self.assertEqual(status, '201 Created')
def test_copy_symlink(self):
req = Request.blank(
'/v1/a/src_cont/symlink?symlink=get', method='COPY',
headers={'Destination': 'tgt_cont/tgt_obj'})
self._test_copy_symlink(req)
req = Request.blank(
'/v1/a/tgt_cont/tgt_obj?symlink=get', method='PUT',
headers={'X-Copy-From': 'src_cont/symlink'})
self._test_copy_symlink(req)
def _test_copy_symlink(self, req):
self.app.register('GET', '/v1/a/src_cont/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated,
{'X-Symlink-Target': 'c1/o',
'X-Symlink-Target-Account': 'a2'})
status, headers, body = self.call_copy(req)
self.assertEqual(status, '201 Created')
method, path, hdrs = self.app.calls_with_headers[0]
self.assertEqual(method, 'GET')
self.assertEqual(path, '/v1/a/src_cont/symlink?symlink=get')
self.assertEqual('/src_cont/symlink', hdrs.get('X-Copy-From'))
method, path, hdrs = self.app.calls_with_headers[1]
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
self.assertEqual(val, 'c1/o')
self.assertEqual(
hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'), 'a2')
def test_copy_symlink_new_target(self):
req = Request.blank(
'/v1/a/src_cont/symlink?symlink=get', method='COPY',
headers={'Destination': 'tgt_cont/tgt_obj',
'X-Symlink-Target': 'new_cont/new_obj',
'X-Symlink-Target-Account': 'new_acct'})
self._test_copy_symlink_new_target(req)
req = Request.blank(
'/v1/a/tgt_cont/tgt_obj?symlink=get', method='PUT',
headers={'X-Copy-From': 'src_cont/symlink',
'X-Symlink-Target': 'new_cont/new_obj',
'X-Symlink-Target-Account': 'new_acct'})
self._test_copy_symlink_new_target(req)
def _test_copy_symlink_new_target(self, req):
self.app.register('GET', '/v1/a/src_cont/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated,
{'X-Symlink-Target': 'c1/o',
'X-Symlink-Target-Account': 'a2'})
status, headers, body = self.call_copy(req)
self.assertEqual(status, '201 Created')
method, path, hdrs = self.app.calls_with_headers[0]
self.assertEqual(method, 'GET')
self.assertEqual(path, '/v1/a/src_cont/symlink?symlink=get')
self.assertEqual('/src_cont/symlink', hdrs.get('X-Copy-From'))
method, path, hdrs = self.app.calls_with_headers[1]
self.assertEqual(method, 'PUT')
self.assertEqual(path, '/v1/a/tgt_cont/tgt_obj?symlink=get')
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
self.assertEqual(val, 'new_cont/new_obj')
self.assertEqual(hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'),
'new_acct')
def test_copy_symlink_with_slo_query(self):
req = Request.blank(
'/v1/a/src_cont/symlink?multipart-manifest=get&symlink=get',
method='COPY', headers={'Destination': 'tgt_cont/tgt_obj'})
self._test_copy_symlink_with_slo_query(req)
req = Request.blank(
'/v1/a/tgt_cont/tgt_obj?multipart-manifest=get&symlink=get',
method='PUT', headers={'X-Copy-From': 'src_cont/symlink'})
self._test_copy_symlink_with_slo_query(req)
def _test_copy_symlink_with_slo_query(self, req):
self.app.register('GET', '/v1/a/src_cont/symlink', swob.HTTPOk,
{'X-Object-Sysmeta-Symlink-Target': 'c1/o',
'X-Object-Sysmeta-Symlink-Target-Account': 'a2'})
self.app.register('PUT', '/v1/a/tgt_cont/tgt_obj', swob.HTTPCreated,
{'X-Symlink-Target': 'c1/o',
'X-Symlink-Target-Account': 'a2'})
status, headers, body = self.call_copy(req)
self.assertEqual(status, '201 Created')
method, path, hdrs = self.app.calls_with_headers[0]
self.assertEqual(method, 'GET')
path, query = path.split('?')
query_dict = parse_qs(query)
self.assertEqual(
path, '/v1/a/src_cont/symlink')
self.assertEqual(
query_dict,
{'multipart-manifest': ['get'], 'symlink': ['get'],
'format': ['raw']})
self.assertEqual('/src_cont/symlink', hdrs.get('X-Copy-From'))
method, path, hdrs = self.app.calls_with_headers[1]
val = hdrs.get('X-Object-Sysmeta-Symlink-Target')
self.assertEqual(val, 'c1/o')
self.assertEqual(
hdrs.get('X-Object-Sysmeta-Symlink-Target-Account'), 'a2')
class SymlinkVersioningTestCase(TestSymlinkMiddlewareBase):
# verify interaction of versioned_writes and symlink middlewares
def setUp(self):
self.app = FakeSwift()
conf = {'symloop_max': '2'}
self.sym = symlink.filter_factory(conf)(self.app)
self.sym.logger = self.app.logger
vw_conf = {'allow_versioned_writes': 'true'}
self.vw = versioned_writes.filter_factory(vw_conf)(self.sym)
def call_vw(self, req, **kwargs):
return self.call_app(req, app=self.vw, **kwargs)
def assertRequestEqual(self, req, other):
self.assertEqual(req.method, other.method)
self.assertEqual(req.path, other.path)
def test_new_symlink_version_success(self):
self.app.register(
'PUT', '/v1/a/c/symlink', swob.HTTPCreated,
{'X-Symlink-Target': 'new_cont/new_tgt',
'X-Symlink-Target-Account': 'a'}, None)
self.app.register(
'GET', '/v1/a/c/symlink', swob.HTTPOk,
{'last-modified': 'Thu, 1 Jan 1970 00:00:01 GMT',
'X-Object-Sysmeta-Symlink-Target': 'old_cont/old_tgt',
'X-Object-Sysmeta-Symlink-Target-Account': 'a'},
'')
self.app.register(
'PUT', '/v1/a/ver_cont/007symlink/0000000001.00000',
swob.HTTPCreated,
{'X-Symlink-Target': 'old_cont/old_tgt',
'X-Symlink-Target-Account': 'a'}, None)
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
req = Request.blank(
'/v1/a/c/symlink',
headers={'X-Symlink-Target': 'new_cont/new_tgt'},
environ={'REQUEST_METHOD': 'PUT', 'swift.cache': cache,
'CONTENT_LENGTH': '0',
'swift.trans_id': 'fake_trans_id'})
status, headers, body = self.call_vw(req)
self.assertEqual(status, '201 Created')
# authorized twice now because versioned_writes now makes a check on
# PUT
self.assertEqual(len(self.authorized), 2)
self.assertRequestEqual(req, self.authorized[0])
self.assertEqual(['VW', 'VW', None], self.app.swift_sources)
self.assertEqual({'fake_trans_id'}, set(self.app.txn_ids))
calls = self.app.calls_with_headers
method, path, req_headers = calls[2]
self.assertEqual('PUT', method)
self.assertEqual('/v1/a/c/symlink', path)
self.assertEqual(
'new_cont/new_tgt',
req_headers['X-Object-Sysmeta-Symlink-Target'])
def test_delete_latest_version_no_marker_success(self):
self.app.register(
'GET',
'/v1/a/ver_cont?prefix=003sym/&marker=&reverse=on',
swob.HTTPOk, {},
'[{"hash": "y", '
'"last_modified": "2014-11-21T14:23:02.206740", '
'"bytes": 0, '
'"name": "003sym/2", '
'"content_type": "text/plain"}, '
'{"hash": "x", '
'"last_modified": "2014-11-21T14:14:27.409100", '
'"bytes": 0, '
'"name": "003sym/1", '
'"content_type": "text/plain"}]')
self.app.register(
'GET', '/v1/a/ver_cont/003sym/2', swob.HTTPCreated,
{'content-length': '0',
'X-Object-Sysmeta-Symlink-Target': 'c/tgt'}, None)
self.app.register(
'PUT', '/v1/a/c/sym', swob.HTTPCreated,
{'X-Symlink-Target': 'c/tgt', 'X-Symlink-Target-Account': 'a'},
None)
self.app.register(
'DELETE', '/v1/a/ver_cont/003sym/2', swob.HTTPOk,
{}, None)
cache = FakeCache({'sysmeta': {'versions-location': 'ver_cont'}})
req = Request.blank(
'/v1/a/c/sym',
headers={'X-If-Delete-At': 1},
environ={'REQUEST_METHOD': 'DELETE', 'swift.cache': cache,
'CONTENT_LENGTH': '0', 'swift.trans_id': 'fake_trans_id'})
status, headers, body = self.call_vw(req)
self.assertEqual(status, '200 OK')
self.assertEqual(len(self.authorized), 1)
self.assertRequestEqual(req, self.authorized[0])
self.assertEqual(4, self.app.call_count)
self.assertEqual(['VW', 'VW', 'VW', 'VW'], self.app.swift_sources)
self.assertEqual({'fake_trans_id'}, set(self.app.txn_ids))
calls = self.app.calls_with_headers
method, path, req_headers = calls[2]
self.assertEqual('PUT', method)
self.assertEqual('/v1/a/c/sym', path)
self.assertEqual(
'c/tgt',
req_headers['X-Object-Sysmeta-Symlink-Target'])
class TestSymlinkContainerContext(TestSymlinkMiddlewareBase):
def setUp(self):
super(TestSymlinkContainerContext, self).setUp()
self.context = symlink.SymlinkContainerContext(
self.sym.app, self.sym.logger)
def test_extract_symlink_path_json_simple_etag(self):
obj_dict = {"bytes": 6,
"last_modified": "1",
"hash": "etag",
"name": "obj",
"content_type": "application/octet-stream"}
obj_dict = self.context._extract_symlink_path_json(
obj_dict, 'v1', 'AUTH_a')
self.assertEqual(obj_dict['hash'], 'etag')
self.assertNotIn('symlink_path', obj_dict)
def test_extract_symlink_path_json_symlink_path(self):
obj_dict = {"bytes": 6,
"last_modified": "1",
"hash": "etag; symlink_target=c/o",
"name": "obj",
"content_type": "application/octet-stream"}
obj_dict = self.context._extract_symlink_path_json(
obj_dict, 'v1', 'AUTH_a')
self.assertEqual(obj_dict['hash'], 'etag')
self.assertEqual(obj_dict['symlink_path'], '/v1/AUTH_a/c/o')
def test_extract_symlink_path_json_symlink_path_and_account(self):
obj_dict = {
"bytes": 6,
"last_modified": "1",
"hash": "etag; symlink_target=c/o; symlink_target_account=AUTH_a2",
"name": "obj",
"content_type": "application/octet-stream"}
obj_dict = self.context._extract_symlink_path_json(
obj_dict, 'v1', 'AUTH_a')
self.assertEqual(obj_dict['hash'], 'etag')
self.assertEqual(obj_dict['symlink_path'], '/v1/AUTH_a2/c/o')
def test_extract_symlink_path_json_extra_key(self):
obj_dict = {"bytes": 6,
"last_modified": "1",
"hash": "etag; symlink_target=c/o; extra_key=value",
"name": "obj",
"content_type": "application/octet-stream"}
obj_dict = self.context._extract_symlink_path_json(
obj_dict, 'v1', 'AUTH_a')
self.assertEqual(obj_dict['hash'], 'etag; extra_key=value')
self.assertEqual(obj_dict['symlink_path'], '/v1/AUTH_a/c/o')
def test_get_container_simple(self):
self.app.register(
'GET',
'/v1/a/c',
swob.HTTPOk, {},
json.dumps(
[{"hash": "etag; symlink_target=c/o;",
"last_modified": "2014-11-21T14:23:02.206740",
"bytes": 0,
"name": "sym_obj",
"content_type": "text/plain"},
{"hash": "etag2",
"last_modified": "2014-11-21T14:14:27.409100",
"bytes": 32,
"name": "normal_obj",
"content_type": "text/plain"}]))
req = Request.blank(path='/v1/a/c')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
obj_list = json.loads(body)
self.assertIn('symlink_path', obj_list[0])
self.assertIn(obj_list[0]['symlink_path'], '/v1/a/c/o')
self.assertNotIn('symlink_path', obj_list[1])
def test_get_container_with_subdir(self):
self.app.register(
'GET',
'/v1/a/c?delimiter=/',
swob.HTTPOk, {},
json.dumps([{"subdir": "photos/"}]))
req = Request.blank(path='/v1/a/c?delimiter=/')
status, headers, body = self.call_sym(req)
self.assertEqual(status, '200 OK')
obj_list = json.loads(body)
self.assertEqual(len(obj_list), 1)
self.assertEqual(obj_list[0]['subdir'], 'photos/')
def test_get_container_error_cases(self):
# No affect for error cases
for error in (swob.HTTPNotFound, swob.HTTPUnauthorized,
swob.HTTPServiceUnavailable,
swob.HTTPInternalServerError):
self.app.register('GET', '/v1/a/c', error, {}, '')
req = Request.blank(path='/v1/a/c')
status, headers, body = self.call_sym(req)
self.assertEqual(status, error().status)
def test_no_affect_for_account_request(self):
with mock.patch.object(self.sym, 'app') as mock_app:
mock_app.return_value = 'ok'
req = Request.blank(path='/v1/a')
status, headers, body = self.call_sym(req)
self.assertEqual(body, 'ok')
def test_get_container_simple_with_listing_format(self):
self.app.register(
'GET',
'/v1/a/c?format=json',
swob.HTTPOk, {},
json.dumps(
[{"hash": "etag; symlink_target=c/o;",
"last_modified": "2014-11-21T14:23:02.206740",
"bytes": 0,
"name": "sym_obj",
"content_type": "text/plain"},
{"hash": "etag2",
"last_modified": "2014-11-21T14:14:27.409100",
"bytes": 32,
"name": "normal_obj",
"content_type": "text/plain"}]))
self.lf = listing_formats.filter_factory({})(self.sym)
req = Request.blank(path='/v1/a/c?format=json')
status, headers, body = self.call_app(req, app=self.lf)
self.assertEqual(status, '200 OK')
obj_list = json.loads(body)
self.assertIn('symlink_path', obj_list[0])
self.assertIn(obj_list[0]['symlink_path'], '/v1/a/c/o')
self.assertNotIn('symlink_path', obj_list[1])
def test_get_container_simple_with_listing_format_xml(self):
self.app.register(
'GET',
'/v1/a/c?format=json',
swob.HTTPOk, {'Content-Type': 'application/json'},
json.dumps(
[{"hash": "etag; symlink_target=c/o;",
"last_modified": "2014-11-21T14:23:02.206740",
"bytes": 0,
"name": "sym_obj",
"content_type": "text/plain"},
{"hash": "etag2",
"last_modified": "2014-11-21T14:14:27.409100",
"bytes": 32,
"name": "normal_obj",
"content_type": "text/plain"}]))
self.lf = listing_formats.filter_factory({})(self.sym)
req = Request.blank(path='/v1/a/c?format=xml')
status, headers, body = self.call_app(req, app=self.lf)
self.assertEqual(status, '200 OK')
self.assertEqual(body.split('\n'), [
'<?xml version="1.0" encoding="UTF-8"?>',
'<container name="c"><object><name>sym_obj</name>'
'<hash>etag</hash><bytes>0</bytes>'
'<content_type>text/plain</content_type>'
'<last_modified>2014-11-21T14:23:02.206740</last_modified>'
'</object>'
'<object><name>normal_obj</name><hash>etag2</hash>'
'<bytes>32</bytes><content_type>text/plain</content_type>'
'<last_modified>2014-11-21T14:14:27.409100</last_modified>'
'</object></container>'])

View File

@ -857,18 +857,21 @@ class TestTempURL(unittest.TestCase):
path = '/v1/a/c/o'
key = 'abc'
for method in ('PUT', 'POST'):
hmac_body = '%s\n%s\n%s' % (method, expires, path)
sig = hmac.new(key, hmac_body, sha1).hexdigest()
req = self._make_request(
path, method=method, keys=[key],
headers={'x-object-manifest': 'private/secret'},
environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s'
% (sig, expires)})
resp = req.get_response(self.tempurl)
self.assertEqual(resp.status_int, 400)
self.assertTrue('header' in resp.body)
self.assertTrue('not allowed' in resp.body)
self.assertTrue('X-Object-Manifest' in resp.body)
for hdr, value in [('X-Object-Manifest', 'private/secret'),
('X-Symlink-Target', 'cont/symlink')]:
hmac_body = '%s\n%s\n%s' % (method, expires, path)
sig = hmac.new(key, hmac_body, sha1).hexdigest()
req = self._make_request(
path, method=method, keys=[key],
headers={hdr: value},
environ={'QUERY_STRING':
'temp_url_sig=%s&temp_url_expires=%s'
% (sig, expires)})
resp = req.get_response(self.tempurl)
self.assertEqual(resp.status_int, 400)
self.assertTrue('header' in resp.body)
self.assertTrue('not allowed' in resp.body)
self.assertTrue(hdr in resp.body)
def test_removed_incoming_header(self):
self.tempurl = tempurl.filter_factory({

View File

@ -1072,6 +1072,11 @@ class TestRingBuilder(unittest.TestCase):
new_dev_parts[dev_id] += 1
for dev in rb._iter_devs():
dev['parts'] = new_dev_parts[dev['id']]
# reset the _last_part_gather_start otherwise
# there is a chance it'll unluckly wrap and try and
# move one of the device 1's from replica 2
# causing the intermitant failure in bug 1724356
rb._last_part_gather_start = 0
rb.pretend_min_part_hours_passed()
rb.rebalance()

View File

@ -23,7 +23,7 @@ import os
import six
from six import StringIO
from six.moves import range
from six.moves.urllib.parse import quote
from six.moves.urllib.parse import quote, parse_qsl
from test.unit import FakeLogger
from swift.common import exceptions, internal_client, swob
from swift.common.header_key_dict import HeaderKeyDict
@ -316,6 +316,29 @@ class TestInternalClient(unittest.TestCase):
client = InternalClient(self)
client.make_request('GET', '/', {}, (200,))
def test_make_request_sets_query_string(self):
captured_envs = []
class InternalClient(internal_client.InternalClient):
def __init__(self, test):
self.test = test
self.app = self.fake_app
self.user_agent = 'some_agent'
self.request_tries = 1
def fake_app(self, env, start_response):
captured_envs.append(env)
start_response('200 Ok', [('Content-Length', '0')])
return []
client = InternalClient(self)
params = {'param1': 'p1', 'tasty': 'soup'}
client.make_request('GET', '/', {}, (200,), params=params)
actual_params = dict(parse_qsl(captured_envs[0]['QUERY_STRING'],
keep_blank_values=True,
strict_parsing=True))
self.assertEqual(params, actual_params)
def test_make_request_retries(self):
class InternalClient(internal_client.InternalClient):
def __init__(self, test):
@ -1047,10 +1070,11 @@ class TestInternalClient(unittest.TestCase):
client, app = get_client_app()
headers = {'foo': 'bar'}
body = 'some_object_body'
params = {'symlink': 'get'}
app.register('GET', path_info, swob.HTTPOk, headers, body)
req_headers = {'x-important-header': 'some_important_value'}
status_int, resp_headers, obj_iter = client.get_object(
account, container, obj, req_headers)
account, container, obj, req_headers, params=params)
self.assertEqual(status_int // 100, 2)
for k, v in headers.items():
self.assertEqual(v, resp_headers[k])
@ -1062,7 +1086,7 @@ class TestInternalClient(unittest.TestCase):
'user-agent': 'test', # from InternalClient.make_request
})
self.assertEqual(app.calls_with_headers, [(
'GET', path_info, HeaderKeyDict(req_headers))])
'GET', path_info + '?symlink=get', HeaderKeyDict(req_headers))])
def test_iter_object_lines(self):
class InternalClient(internal_client.InternalClient):

View File

@ -41,6 +41,7 @@ import string
import sys
import json
import math
import inspect
import six
from six import BytesIO, StringIO
@ -3976,6 +3977,54 @@ cluster_dfw1 = http://dfw1.host/v1/
self.fail('Invalid results from pure function:\n%s' %
'\n'.join(failures))
def test_strict_b64decode(self):
expectations = {
None: ValueError,
0: ValueError,
b'': b'',
u'': b'',
b'A': ValueError,
b'AA': ValueError,
b'AAA': ValueError,
b'AAAA': b'\x00\x00\x00',
u'AAAA': b'\x00\x00\x00',
b'////': b'\xff\xff\xff',
u'////': b'\xff\xff\xff',
b'A===': ValueError,
b'AA==': b'\x00',
b'AAA=': b'\x00\x00',
b' AAAA': ValueError,
b'AAAA ': ValueError,
b'AAAA============': b'\x00\x00\x00',
b'AA&AA==': ValueError,
b'====': b'',
}
failures = []
for value, expected in expectations.items():
try:
result = utils.strict_b64decode(value)
except Exception as e:
if inspect.isclass(expected) and issubclass(
expected, Exception):
if not isinstance(e, expected):
failures.append('%r raised %r (expected to raise %r)' %
(value, e, expected))
else:
failures.append('%r raised %r (expected to return %r)' %
(value, e, expected))
else:
if inspect.isclass(expected) and issubclass(
expected, Exception):
failures.append('%r => %r (expected to raise %r)' %
(value, result, expected))
elif result != expected:
failures.append('%r => %r (expected %r)' % (
value, result, expected))
if failures:
self.fail('Invalid results from pure function:\n%s' %
'\n'.join(failures))
def test_replace_partition_in_path(self):
# Check for new part = part * 2
old = '/s/n/d/o/700/c77/af088baea4806dcaba30bf07d9e64c77/f'

View File

@ -198,6 +198,97 @@ class TestWSGI(unittest.TestCase):
app = wsgi.loadapp(wsgi.ConfigString(conf_body))
self.assertTrue(isinstance(app, obj_server.ObjectController))
@with_tempdir
def test_load_app_config(self, tempdir):
conf_file = os.path.join(tempdir, 'file.conf')
def _write_and_load_conf_file(conf):
with open(conf_file, 'wb') as fd:
fd.write(dedent(conf))
return wsgi.load_app_config(conf_file)
# typical case - DEFAULT options override same option in other sections
conf_str = """
[DEFAULT]
dflt_option = dflt-value
[pipeline:main]
pipeline = proxy-logging proxy-server
[filter:proxy-logging]
use = egg:swift#proxy_logging
[app:proxy-server]
use = egg:swift#proxy
proxy_option = proxy-value
dflt_option = proxy-dflt-value
"""
proxy_conf = _write_and_load_conf_file(conf_str)
self.assertEqual('proxy-value', proxy_conf['proxy_option'])
self.assertEqual('dflt-value', proxy_conf['dflt_option'])
# 'set' overrides DEFAULT option
conf_str = """
[DEFAULT]
dflt_option = dflt-value
[pipeline:main]
pipeline = proxy-logging proxy-server
[filter:proxy-logging]
use = egg:swift#proxy_logging
[app:proxy-server]
use = egg:swift#proxy
proxy_option = proxy-value
set dflt_option = proxy-dflt-value
"""
proxy_conf = _write_and_load_conf_file(conf_str)
self.assertEqual('proxy-value', proxy_conf['proxy_option'])
self.assertEqual('proxy-dflt-value', proxy_conf['dflt_option'])
# actual proxy server app name is dereferenced
conf_str = """
[pipeline:main]
pipeline = proxy-logging proxyserverapp
[filter:proxy-logging]
use = egg:swift#proxy_logging
[app:proxyserverapp]
use = egg:swift#proxy
proxy_option = proxy-value
dflt_option = proxy-dflt-value
"""
proxy_conf = _write_and_load_conf_file(conf_str)
self.assertEqual('proxy-value', proxy_conf['proxy_option'])
self.assertEqual('proxy-dflt-value', proxy_conf['dflt_option'])
# no pipeline
conf_str = """
[filter:proxy-logging]
use = egg:swift#proxy_logging
[app:proxy-server]
use = egg:swift#proxy
proxy_option = proxy-value
"""
proxy_conf = _write_and_load_conf_file(conf_str)
self.assertEqual({}, proxy_conf)
# no matching section
conf_str = """
[pipeline:main]
pipeline = proxy-logging proxy-server
[filter:proxy-logging]
use = egg:swift#proxy_logging
"""
proxy_conf = _write_and_load_conf_file(conf_str)
self.assertEqual({}, proxy_conf)
def test_init_request_processor_from_conf_dir(self):
config_dir = {
'proxy-server.conf.d/pipeline.conf': """

View File

@ -968,7 +968,9 @@ class TestContainerSync(unittest.TestCase):
logger=self.logger)
cs.http_proxies = ['http://proxy']
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')
return (200,
@ -1004,7 +1006,9 @@ class TestContainerSync(unittest.TestCase):
expected_put_count += 1
self.assertEqual(cs.container_puts, expected_put_count)
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Newest'], True)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')
@ -1055,7 +1059,9 @@ class TestContainerSync(unittest.TestCase):
expected_put_count += 1
self.assertEqual(cs.container_puts, expected_put_count)
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Newest'], True)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')
@ -1090,7 +1096,9 @@ class TestContainerSync(unittest.TestCase):
exc = []
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Newest'], True)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')
@ -1114,7 +1122,9 @@ class TestContainerSync(unittest.TestCase):
exc = []
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Newest'], True)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')
@ -1137,7 +1147,9 @@ class TestContainerSync(unittest.TestCase):
self.assertEqual(len(exc), 1)
self.assertEqual(str(exc[-1]), 'test client exception')
def fake_get_object(acct, con, obj, headers, acceptable_statuses):
def fake_get_object(acct, con, obj, headers, acceptable_statuses,
params=None):
self.assertEqual({'symlink': 'get'}, params)
self.assertEqual(headers['X-Newest'], True)
self.assertEqual(headers['X-Backend-Storage-Policy-Index'],
'0')

View File

@ -2235,11 +2235,11 @@ class TestECDiskFileManager(DiskFileManagerMixin, unittest.TestCase):
rmtree(df._datadir, ignore_errors=True)
# sanity
files = [
good_files = [
'0000000006.00000.meta',
'0000000006.00000#1#d.data'
]
with create_files(class_under_test, files):
with create_files(class_under_test, good_files):
class_under_test.open()
scenarios = [['0000000007.00000.meta'],
@ -2263,6 +2263,22 @@ class TestECDiskFileManager(DiskFileManagerMixin, unittest.TestCase):
self.fail('expected DiskFileNotExist opening %s with %r' % (
class_under_test.__class__.__name__, files))
# Simulate another process deleting the data after we list contents
# but before we actually open them
orig_listdir = os.listdir
def deleting_listdir(d):
result = orig_listdir(d)
for f in result:
os.unlink(os.path.join(d, f))
return result
with create_files(class_under_test, good_files), \
mock.patch('swift.obj.diskfile.os.listdir',
side_effect=deleting_listdir), \
self.assertRaises(DiskFileNotExist):
class_under_test.open()
def test_verify_ondisk_files(self):
# _verify_ondisk_files should only return False if get_ondisk_files
# has produced a bad set of files due to a bug, so to test it we need

View File

@ -30,8 +30,7 @@ from eventlet.green import subprocess
from eventlet import Timeout
from test.unit import (debug_logger, patch_policies, make_timestamp_iter,
mocked_http_conn, FakeLogger, mock_check_drive,
skip_if_no_xattrs)
mocked_http_conn, mock_check_drive, skip_if_no_xattrs)
from swift.common import utils
from swift.common.utils import (hash_path, mkdirs, normalize_timestamp,
storage_directory)
@ -1558,6 +1557,7 @@ class TestObjectReplicator(unittest.TestCase):
def raise_exception_rmdir(exception_class, error_no):
instance = exception_class()
instance.errno = error_no
instance.strerror = os.strerror(error_no)
def func(directory):
if directory == suffix_dir_path:
@ -1596,7 +1596,10 @@ class TestObjectReplicator(unittest.TestCase):
with mock.patch('os.rmdir',
raise_exception_rmdir(OSError, ENOTDIR)):
self.replicator.replicate()
self.assertEqual(len(mock_logger.get_lines_for_level('error')), 1)
self.assertEqual(mock_logger.get_lines_for_level('error'), [
'Unexpected error trying to cleanup suffix dir:%r: ' %
os.path.dirname(df._datadir),
])
self.assertFalse(os.access(whole_path_from, os.F_OK))
self.assertTrue(os.access(suffix_dir_path, os.F_OK))
self.assertTrue(os.access(part_path, os.F_OK))
@ -1692,7 +1695,7 @@ class TestObjectReplicator(unittest.TestCase):
mock_http_connect(200)):
with mock.patch('swift.obj.replicator.dump_recon_cache'):
replicator = object_replicator.ObjectReplicator(
conf, logger=FakeLogger())
conf, logger=self.logger)
self.get_hash_count = 0
with mock.patch.object(replicator, 'sync', fake_sync):
@ -1843,6 +1846,7 @@ class TestObjectReplicator(unittest.TestCase):
# Check successful http_connection and exception with
# incorrect pickle.loads(resp.read())
resp.status = 200
resp.read.return_value = 'garbage'
expect = 'Error syncing with node: %r: '
for job in jobs:
set_default(self)

30
tools/test-setup.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash -xe
# Set up a partition formatted with XFS to use as TMPDIR for our tests.
# OpenStack CI will invoke this script as part of tox based tests.
# The file .zuul.yaml set TMPDIR to $HOME/xfstmp.
# Create a large-ish file that we will mount as a loopback
truncate -s 1GB $HOME/1G_xfs_file
# Format the new file as XFS.
/sbin/mkfs.xfs $HOME/1G_xfs_file
# loopback mount the file
mkdir -p $HOME/xfstmp
sudo mount -o loop,noatime,nodiratime $HOME/1G_xfs_file $HOME/xfstmp
sudo chmod 777 $HOME/xfstmp
# Install liberasurecode-devel for CentOS from RDO repository.
function is_rhel7 {
[ -f /usr/bin/yum ] && \
cat /etc/*release | grep -q -e "Red Hat" -e "CentOS" -e "CloudLinux" && \
cat /etc/*release | grep -q 'release 7'
}
if is_rhel7; then
# Install CentOS OpenStack repos so that we have access to some extra
# packages.
sudo yum install -y centos-release-openstack-pike
sudo yum install -y liberasurecode-devel
fi