merge up to trunk, rollback xml
This commit is contained in:
commit
d234461344
119
bin/swift
119
bin/swift
|
@ -150,22 +150,27 @@ class ClientException(Exception):
|
||||||
return b and '%s: %s' % (a, b) or a
|
return b and '%s: %s' % (a, b) or a
|
||||||
|
|
||||||
|
|
||||||
def http_connection(url):
|
def http_connection(url, proxy=None):
|
||||||
"""
|
"""
|
||||||
Make an HTTPConnection or HTTPSConnection
|
Make an HTTPConnection or HTTPSConnection
|
||||||
|
|
||||||
:param url: url to connect to
|
:param url: url to connect to
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:returns: tuple of (parsed url, connection object)
|
:returns: tuple of (parsed url, connection object)
|
||||||
:raises ClientException: Unable to handle protocol scheme
|
:raises ClientException: Unable to handle protocol scheme
|
||||||
"""
|
"""
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
|
proxy_parsed = urlparse(proxy) if proxy else None
|
||||||
if parsed.scheme == 'http':
|
if parsed.scheme == 'http':
|
||||||
conn = HTTPConnection(parsed.netloc)
|
conn = HTTPConnection((proxy_parsed if proxy else parsed).netloc)
|
||||||
elif parsed.scheme == 'https':
|
elif parsed.scheme == 'https':
|
||||||
conn = HTTPSConnection(parsed.netloc)
|
conn = HTTPSConnection((proxy_parsed if proxy else parsed).netloc)
|
||||||
else:
|
else:
|
||||||
raise ClientException('Cannot handle protocol scheme %s for url %s' %
|
raise ClientException('Cannot handle protocol scheme %s for url %s' %
|
||||||
(parsed.scheme, repr(url)))
|
(parsed.scheme, repr(url)))
|
||||||
|
if proxy:
|
||||||
|
conn._set_tunnel(parsed.hostname, parsed.port)
|
||||||
return parsed, conn
|
return parsed, conn
|
||||||
|
|
||||||
|
|
||||||
|
@ -578,40 +583,60 @@ def head_object(url, token, container, name, http_conn=None):
|
||||||
return resp_headers
|
return resp_headers
|
||||||
|
|
||||||
|
|
||||||
def put_object(url, token, container, name, contents, content_length=None,
|
def put_object(url, token=None, container=None, name=None, contents=None,
|
||||||
etag=None, chunk_size=65536, content_type=None, headers=None,
|
content_length=None, etag=None, chunk_size=65536,
|
||||||
http_conn=None):
|
content_type=None, headers=None, http_conn=None, proxy=None):
|
||||||
"""
|
"""
|
||||||
Put an object
|
Put an object
|
||||||
|
|
||||||
:param url: storage URL
|
:param url: storage URL
|
||||||
:param token: auth token
|
:param token: auth token; if None, no token will be sent
|
||||||
:param container: container name that the object is in
|
:param container: container name that the object is in; if None, the
|
||||||
:param name: object name to put
|
container name is expected to be part of the url
|
||||||
:param contents: a string or a file like object to read object data from
|
:param name: object name to put; if None, the object name is expected to be
|
||||||
|
part of the url
|
||||||
|
:param contents: a string or a file like object to read object data from;
|
||||||
|
if None, a zero-byte put will be done
|
||||||
:param content_length: value to send as content-length header; also limits
|
:param content_length: value to send as content-length header; also limits
|
||||||
the amount read from contents
|
the amount read from contents; if None, it will be
|
||||||
:param etag: etag of contents
|
computed via the contents or chunked transfer
|
||||||
:param chunk_size: chunk size of data to write
|
encoding will be used
|
||||||
:param content_type: value to send as content-type header
|
:param etag: etag of contents; if None, no etag will be sent
|
||||||
:param headers: additional headers to include in the request
|
:param chunk_size: chunk size of data to write; default 65536
|
||||||
|
:param content_type: value to send as content-type header; if None, no
|
||||||
|
content-type will be set (remote end will likely try
|
||||||
|
to auto-detect it)
|
||||||
|
:param headers: additional headers to include in the request, if any
|
||||||
:param http_conn: HTTP connection object (If None, it will create the
|
:param http_conn: HTTP connection object (If None, it will create the
|
||||||
conn object)
|
conn object)
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:returns: etag from server response
|
:returns: etag from server response
|
||||||
:raises ClientException: HTTP PUT request failed
|
:raises ClientException: HTTP PUT request failed
|
||||||
"""
|
"""
|
||||||
if http_conn:
|
if http_conn:
|
||||||
parsed, conn = http_conn
|
parsed, conn = http_conn
|
||||||
else:
|
else:
|
||||||
parsed, conn = http_connection(url)
|
parsed, conn = http_connection(url, proxy=proxy)
|
||||||
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
path = parsed.path
|
||||||
if not headers:
|
if container:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
||||||
|
if name:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
||||||
|
if headers:
|
||||||
|
headers = dict(headers)
|
||||||
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['X-Auth-Token'] = token
|
if token:
|
||||||
|
headers['X-Auth-Token'] = token
|
||||||
if etag:
|
if etag:
|
||||||
headers['ETag'] = etag.strip('"')
|
headers['ETag'] = etag.strip('"')
|
||||||
if content_length is not None:
|
if content_length is not None:
|
||||||
headers['Content-Length'] = str(content_length)
|
headers['Content-Length'] = str(content_length)
|
||||||
|
else:
|
||||||
|
for n, v in headers.iteritems():
|
||||||
|
if n.lower() == 'content-length':
|
||||||
|
content_length = int(v)
|
||||||
if content_type is not None:
|
if content_type is not None:
|
||||||
headers['Content-Type'] = content_type
|
headers['Content-Type'] = content_type
|
||||||
if not contents:
|
if not contents:
|
||||||
|
@ -646,7 +671,7 @@ def put_object(url, token, container, name, contents, content_length=None,
|
||||||
raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
|
raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
|
||||||
http_host=conn.host, http_port=conn.port, http_path=path,
|
http_host=conn.host, http_port=conn.port, http_path=path,
|
||||||
http_status=resp.status, http_reason=resp.reason)
|
http_status=resp.status, http_reason=resp.reason)
|
||||||
return resp.getheader('etag').strip('"')
|
return resp.getheader('etag', '').strip('"')
|
||||||
|
|
||||||
|
|
||||||
def post_object(url, token, container, name, headers, http_conn=None):
|
def post_object(url, token, container, name, headers, http_conn=None):
|
||||||
|
@ -677,24 +702,40 @@ def post_object(url, token, container, name, headers, http_conn=None):
|
||||||
http_status=resp.status, http_reason=resp.reason)
|
http_status=resp.status, http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def delete_object(url, token, container, name, http_conn=None):
|
def delete_object(url, token=None, container=None, name=None, http_conn=None,
|
||||||
|
headers=None, proxy=None):
|
||||||
"""
|
"""
|
||||||
Delete object
|
Delete object
|
||||||
|
|
||||||
:param url: storage URL
|
:param url: storage URL
|
||||||
:param token: auth token
|
:param token: auth token; if None, no token will be sent
|
||||||
:param container: container name that the object is in
|
:param container: container name that the object is in; if None, the
|
||||||
:param name: object name to delete
|
container name is expected to be part of the url
|
||||||
|
:param name: object name to delete; if None, the object name is expected to
|
||||||
|
be part of the url
|
||||||
:param http_conn: HTTP connection object (If None, it will create the
|
:param http_conn: HTTP connection object (If None, it will create the
|
||||||
conn object)
|
conn object)
|
||||||
|
:param headers: additional headers to include in the request
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:raises ClientException: HTTP DELETE request failed
|
:raises ClientException: HTTP DELETE request failed
|
||||||
"""
|
"""
|
||||||
if http_conn:
|
if http_conn:
|
||||||
parsed, conn = http_conn
|
parsed, conn = http_conn
|
||||||
else:
|
else:
|
||||||
parsed, conn = http_connection(url)
|
parsed, conn = http_connection(url, proxy=proxy)
|
||||||
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
path = parsed.path
|
||||||
conn.request('DELETE', path, '', {'X-Auth-Token': token})
|
if container:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
||||||
|
if name:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
||||||
|
if headers:
|
||||||
|
headers = dict(headers)
|
||||||
|
else:
|
||||||
|
headers = {}
|
||||||
|
if token:
|
||||||
|
headers['X-Auth-Token'] = token
|
||||||
|
conn.request('DELETE', path, '', headers)
|
||||||
resp = conn.getresponse()
|
resp = conn.getresponse()
|
||||||
resp.read()
|
resp.read()
|
||||||
if resp.status < 200 or resp.status >= 300:
|
if resp.status < 200 or resp.status >= 300:
|
||||||
|
@ -1363,10 +1404,14 @@ Container: %s
|
||||||
Objects: %d
|
Objects: %d
|
||||||
Bytes: %d
|
Bytes: %d
|
||||||
Read ACL: %s
|
Read ACL: %s
|
||||||
Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0],
|
Write ACL: %s
|
||||||
|
Sync To: %s
|
||||||
|
Sync Key: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0],
|
||||||
object_count, bytes_used,
|
object_count, bytes_used,
|
||||||
headers.get('x-container-read', ''),
|
headers.get('x-container-read', ''),
|
||||||
headers.get('x-container-write', '')))
|
headers.get('x-container-write', ''),
|
||||||
|
headers.get('x-container-sync-to', ''),
|
||||||
|
headers.get('x-container-sync-key', '')))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if key.startswith('x-container-meta-'):
|
if key.startswith('x-container-meta-'):
|
||||||
print_queue.put('%9s: %s' % ('Meta %s' %
|
print_queue.put('%9s: %s' % ('Meta %s' %
|
||||||
|
@ -1375,7 +1420,8 @@ Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0],
|
||||||
if not key.startswith('x-container-meta-') and key not in (
|
if not key.startswith('x-container-meta-') and key not in (
|
||||||
'content-length', 'date', 'x-container-object-count',
|
'content-length', 'date', 'x-container-object-count',
|
||||||
'x-container-bytes-used', 'x-container-read',
|
'x-container-bytes-used', 'x-container-read',
|
||||||
'x-container-write'):
|
'x-container-write', 'x-container-sync-to',
|
||||||
|
'x-container-sync-key'):
|
||||||
print_queue.put(
|
print_queue.put(
|
||||||
'%9s: %s' % (key.title(), value))
|
'%9s: %s' % (key.title(), value))
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
|
@ -1440,13 +1486,18 @@ def st_post(options, args, print_queue, error_queue):
|
||||||
parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the '
|
parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the '
|
||||||
'Write ACL for containers. Quick summary of ACL syntax: account1, '
|
'Write ACL for containers. Quick summary of ACL syntax: account1, '
|
||||||
'account2:user2')
|
'account2:user2')
|
||||||
|
parser.add_option('-t', '--sync-to', dest='sync_to', help='Sets the '
|
||||||
|
'Sync To for containers, for multi-cluster replication.')
|
||||||
|
parser.add_option('-k', '--sync-key', dest='sync_key', help='Sets the '
|
||||||
|
'Sync Key for containers, for multi-cluster replication.')
|
||||||
parser.add_option('-m', '--meta', action='append', dest='meta', default=[],
|
parser.add_option('-m', '--meta', action='append', dest='meta', default=[],
|
||||||
help='Sets a meta data item with the syntax name:value. This option '
|
help='Sets a meta data item with the syntax name:value. This option '
|
||||||
'may be repeated. Example: -m Color:Blue -m Size:Large')
|
'may be repeated. Example: -m Color:Blue -m Size:Large')
|
||||||
(options, args) = parse_args(parser, args)
|
(options, args) = parse_args(parser, args)
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
if (options.read_acl or options.write_acl) and not args:
|
if (options.read_acl or options.write_acl or options.sync_to or
|
||||||
exit('-r and -w options only allowed for containers')
|
options.sync_key) and not args:
|
||||||
|
exit('-r, -w, -t, and -k options only allowed for containers')
|
||||||
conn = Connection(options.auth, options.user, options.key)
|
conn = Connection(options.auth, options.user, options.key)
|
||||||
if not args:
|
if not args:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
@ -1474,6 +1525,10 @@ def st_post(options, args, print_queue, error_queue):
|
||||||
headers['X-Container-Read'] = options.read_acl
|
headers['X-Container-Read'] = options.read_acl
|
||||||
if options.write_acl is not None:
|
if options.write_acl is not None:
|
||||||
headers['X-Container-Write'] = options.write_acl
|
headers['X-Container-Write'] = options.write_acl
|
||||||
|
if options.sync_to is not None:
|
||||||
|
headers['X-Container-Sync-To'] = options.sync_to
|
||||||
|
if options.sync_key is not None:
|
||||||
|
headers['X-Container-Sync-Key'] = options.sync_key
|
||||||
try:
|
try:
|
||||||
conn.post_container(args[0], headers=headers)
|
conn.post_container(args[0], headers=headers)
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from swift.container.sync import ContainerSync
|
||||||
|
from swift.common.utils import parse_options
|
||||||
|
from swift.common.daemon import run_daemon
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
conf_file, options = parse_options(once=True)
|
||||||
|
run_daemon(ContainerSync, conf_file, **options)
|
|
@ -34,3 +34,10 @@ Container Auditor
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
Container Sync
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: swift.container.sync
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
|
@ -400,6 +400,8 @@ Sample configuration files are provided with all defaults in line-by-line commen
|
||||||
|
|
||||||
[container-auditor]
|
[container-auditor]
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
|
||||||
#. Create `/etc/swift/container-server/2.conf`::
|
#. Create `/etc/swift/container-server/2.conf`::
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
@ -422,6 +424,8 @@ Sample configuration files are provided with all defaults in line-by-line commen
|
||||||
|
|
||||||
[container-auditor]
|
[container-auditor]
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
|
||||||
#. Create `/etc/swift/container-server/3.conf`::
|
#. Create `/etc/swift/container-server/3.conf`::
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
@ -444,6 +448,8 @@ Sample configuration files are provided with all defaults in line-by-line commen
|
||||||
|
|
||||||
[container-auditor]
|
[container-auditor]
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
|
||||||
#. Create `/etc/swift/container-server/4.conf`::
|
#. Create `/etc/swift/container-server/4.conf`::
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
@ -466,6 +472,8 @@ Sample configuration files are provided with all defaults in line-by-line commen
|
||||||
|
|
||||||
[container-auditor]
|
[container-auditor]
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
|
||||||
|
|
||||||
#. Create `/etc/swift/object-server/1.conf`::
|
#. Create `/etc/swift/object-server/1.conf`::
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ Overview and Concepts
|
||||||
overview_replication
|
overview_replication
|
||||||
ratelimit
|
ratelimit
|
||||||
overview_large_objects
|
overview_large_objects
|
||||||
|
overview_container_sync
|
||||||
|
|
||||||
Developer Documentation
|
Developer Documentation
|
||||||
=======================
|
=======================
|
||||||
|
|
|
@ -27,11 +27,17 @@ validation.
|
||||||
Swift will make calls to the auth system, giving the auth token to be
|
Swift will make calls to the auth system, giving the auth token to be
|
||||||
validated. For a valid token, the auth system responds with an overall
|
validated. For a valid token, the auth system responds with an overall
|
||||||
expiration in seconds from now. Swift will cache the token up to the expiration
|
expiration in seconds from now. Swift will cache the token up to the expiration
|
||||||
time. The included TempAuth also has the concept of admin and non-admin users
|
time.
|
||||||
within an account. Admin users can do anything within the account. Non-admin
|
|
||||||
users can only perform operations per container based on the container's
|
The included TempAuth also has the concept of admin and non-admin users within
|
||||||
X-Container-Read and X-Container-Write ACLs. For more information on ACLs, see
|
an account. Admin users can do anything within the account. Non-admin users can
|
||||||
:mod:`swift.common.middleware.acl`
|
only perform operations per container based on the container's X-Container-Read
|
||||||
|
and X-Container-Write ACLs. For more information on ACLs, see
|
||||||
|
:mod:`swift.common.middleware.acl`.
|
||||||
|
|
||||||
|
Additionally, if the auth system sets the request environ's swift_owner key to
|
||||||
|
True, the proxy will return additional header information in some requests,
|
||||||
|
such as the X-Container-Sync-Key for a container GET or HEAD.
|
||||||
|
|
||||||
The user starts a session by sending a ReST request to the auth system to
|
The user starts a session by sending a ReST request to the auth system to
|
||||||
receive the auth token and a URL to the Swift system.
|
receive the auth token and a URL to the Swift system.
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
======================================
|
||||||
|
Container to Container Synchronization
|
||||||
|
======================================
|
||||||
|
|
||||||
|
--------
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
Swift has a feature where all the contents of a container can be mirrored to
|
||||||
|
another container through background synchronization. Swift cluster operators
|
||||||
|
configure their cluster to allow/accept sync requests to/from other clusters,
|
||||||
|
and the user specifies where to sync their container to along with a secret
|
||||||
|
synchronization key.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Container sync will sync object POSTs only if the proxy server is set to
|
||||||
|
use "object_post_as_copy = true" which is the default. So-called fast
|
||||||
|
object posts, "object_post_as_copy = false" do not update the container
|
||||||
|
listings and therefore can't be detected for synchronization.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you are using the large objects feature you will need to ensure both
|
||||||
|
your manifest file and your segment files are synced if they happen to be
|
||||||
|
in different containers.
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Configuring a Cluster's Allowable Sync Hosts
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
The Swift cluster operator must allow synchronization with a set of hosts
|
||||||
|
before the user can enable container synchronization. First, the backend
|
||||||
|
container server needs to be given this list of hosts in the
|
||||||
|
container-server.conf file::
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
# This is a comma separated list of hosts allowed in the
|
||||||
|
# X-Container-Sync-To field for containers.
|
||||||
|
# allowed_sync_hosts = 127.0.0.1
|
||||||
|
allowed_sync_hosts = host1,host2,etc.
|
||||||
|
...
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
# You can override the default log routing for this app here (don't
|
||||||
|
# use set!):
|
||||||
|
# log_name = container-sync
|
||||||
|
# log_facility = LOG_LOCAL0
|
||||||
|
# log_level = INFO
|
||||||
|
# Will sync, at most, each container once per interval
|
||||||
|
# interval = 300
|
||||||
|
# Maximum amount of time to spend syncing each container
|
||||||
|
# container_time = 60
|
||||||
|
|
||||||
|
Tracking sync progress, problems, and just general activity can only be
|
||||||
|
achieved with log processing for this first release of container
|
||||||
|
synchronization. In that light, you may wish to set the above `log_` options to
|
||||||
|
direct the container-sync logs to a different file for easier monitoring.
|
||||||
|
Additionally, it should be noted there is no way for an end user to detect sync
|
||||||
|
progress or problems other than HEADing both containers and comparing the
|
||||||
|
overall information.
|
||||||
|
|
||||||
|
The authentication system also needs to be configured to allow synchronization
|
||||||
|
requests. Here is an example with TempAuth::
|
||||||
|
|
||||||
|
[filter:tempauth]
|
||||||
|
# This is a comma separated list of hosts allowed to send
|
||||||
|
# X-Container-Sync-Key requests.
|
||||||
|
# allowed_sync_hosts = 127.0.0.1
|
||||||
|
allowed_sync_hosts = host1,host2,etc.
|
||||||
|
|
||||||
|
The default of 127.0.0.1 is just so no configuration is required for SAIO
|
||||||
|
setups -- for testing.
|
||||||
|
|
||||||
|
----------------------------------------------------------
|
||||||
|
Using the ``swift`` tool to set up synchronized containers
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You must be the account admin on the account to set synchronization targets
|
||||||
|
and keys.
|
||||||
|
|
||||||
|
You simply tell each container where to sync to and give it a secret
|
||||||
|
synchronization key. First, let's get the account details for our two cluster
|
||||||
|
accounts::
|
||||||
|
|
||||||
|
$ swift -A http://cluster1/auth/v1.0 -U test:tester -K testing stat -v
|
||||||
|
StorageURL: http://cluster1/v1/AUTH_208d1854-e475-4500-b315-81de645d060e
|
||||||
|
Auth Token: AUTH_tkd5359e46ff9e419fa193dbd367f3cd19
|
||||||
|
Account: AUTH_208d1854-e475-4500-b315-81de645d060e
|
||||||
|
Containers: 0
|
||||||
|
Objects: 0
|
||||||
|
Bytes: 0
|
||||||
|
|
||||||
|
$ swift -A http://cluster2/auth/v1.0 -U test2:tester2 -K testing2 stat -v
|
||||||
|
StorageURL: http://cluster2/v1/AUTH_33cdcad8-09fb-4940-90da-0f00cbf21c7c
|
||||||
|
Auth Token: AUTH_tk816a1aaf403c49adb92ecfca2f88e430
|
||||||
|
Account: AUTH_33cdcad8-09fb-4940-90da-0f00cbf21c7c
|
||||||
|
Containers: 0
|
||||||
|
Objects: 0
|
||||||
|
Bytes: 0
|
||||||
|
|
||||||
|
Now, let's make our first container and tell it to synchronize to a second
|
||||||
|
we'll make next::
|
||||||
|
|
||||||
|
$ swift -A http://cluster1/auth/v1.0 -U test:tester -K testing post \
|
||||||
|
-t 'http://cluster2/v1/AUTH_33cdcad8-09fb-4940-90da-0f00cbf21c7c/container2' \
|
||||||
|
-k 'secret' container1
|
||||||
|
|
||||||
|
The ``-t`` indicates the URL to sync to, which is the ``StorageURL`` from
|
||||||
|
cluster2 we retrieved above plus the container name. The ``-k`` specifies the
|
||||||
|
secret key the two containers will share for synchronization. Now, we'll do
|
||||||
|
something similar for the second cluster's container::
|
||||||
|
|
||||||
|
$ swift -A http://cluster2/auth/v1.0 -U test2:tester2 -K testing2 post \
|
||||||
|
-t 'http://cluster1/v1/AUTH_208d1854-e475-4500-b315-81de645d060e/container1' \
|
||||||
|
-k 'secret' container2
|
||||||
|
|
||||||
|
That's it. Now we can upload a bunch of stuff to the first container and watch
|
||||||
|
as it gets synchronized over to the second::
|
||||||
|
|
||||||
|
$ swift -A http://cluster1/auth/v1.0 -U test:tester -K testing \
|
||||||
|
upload container1 .
|
||||||
|
photo002.png
|
||||||
|
photo004.png
|
||||||
|
photo001.png
|
||||||
|
photo003.png
|
||||||
|
|
||||||
|
$ swift -A http://cluster2/auth/v1.0 -U test2:tester2 -K testing2 \
|
||||||
|
list container2
|
||||||
|
|
||||||
|
[Nothing there yet, so we wait a bit...]
|
||||||
|
[If you're an operator running SAIO and just testing, you may need to
|
||||||
|
run 'swift-init container-sync once' to perform a sync scan.]
|
||||||
|
|
||||||
|
$ swift -A http://cluster2/auth/v1.0 -U test2:tester2 -K testing2 \
|
||||||
|
list container2
|
||||||
|
photo001.png
|
||||||
|
photo002.png
|
||||||
|
photo003.png
|
||||||
|
photo004.png
|
||||||
|
|
||||||
|
You can also set up a chain of synced containers if you want more than two.
|
||||||
|
You'd point 1 -> 2, then 2 -> 3, and finally 3 -> 1 for three containers.
|
||||||
|
They'd all need to share the same secret synchronization key.
|
||||||
|
|
||||||
|
-----------------------------------
|
||||||
|
Using curl (or other tools) instead
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
So what's ``swift`` doing behind the scenes? Nothing overly complicated. It
|
||||||
|
translates the ``-t <value>`` option into an ``X-Container-Sync-To: <value>``
|
||||||
|
header and the ``-k <value>`` option into an ``X-Container-Sync-Key: <value>``
|
||||||
|
header.
|
||||||
|
|
||||||
|
For instance, when we created the first container above and told it to
|
||||||
|
synchronize to the second, we could have used this curl command::
|
||||||
|
|
||||||
|
$ curl -i -X POST -H 'X-Auth-Token: AUTH_tkd5359e46ff9e419fa193dbd367f3cd19' \
|
||||||
|
-H 'X-Container-Sync-To: http://cluster2/v1/AUTH_33cdcad8-09fb-4940-90da-0f00cbf21c7c/container2' \
|
||||||
|
-H 'X-Container-Sync-Key: secret' \
|
||||||
|
'http://cluster1/v1/AUTH_208d1854-e475-4500-b315-81de645d060e/container1'
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Content-Length: 0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Date: Thu, 24 Feb 2011 22:39:14 GMT
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
What's going on behind the scenes, in the cluster?
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
The swift-container-sync does the job of sending updates to the remote
|
||||||
|
container.
|
||||||
|
|
||||||
|
This is done by scanning the local devices for container databases and
|
||||||
|
checking for x-container-sync-to and x-container-sync-key metadata values.
|
||||||
|
If they exist, newer rows since the last sync will trigger PUTs or DELETEs
|
||||||
|
to the other container.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Container sync will sync object POSTs only if the proxy server is set to
|
||||||
|
use "object_post_as_copy = true" which is the default. So-called fast
|
||||||
|
object posts, "object_post_as_copy = false" do not update the container
|
||||||
|
listings and therefore can't be detected for synchronization.
|
||||||
|
|
||||||
|
The actual syncing is slightly more complicated to make use of the three
|
||||||
|
(or number-of-replicas) main nodes for a container without each trying to
|
||||||
|
do the exact same work but also without missing work if one node happens to
|
||||||
|
be down.
|
||||||
|
|
||||||
|
Two sync points are kept per container database. All rows between the two
|
||||||
|
sync points trigger updates. Any rows newer than both sync points cause
|
||||||
|
updates depending on the node's position for the container (primary nodes
|
||||||
|
do one third, etc. depending on the replica count of course). After a sync
|
||||||
|
run, the first sync point is set to the newest ROWID known and the second
|
||||||
|
sync point is set to newest ROWID for which all updates have been sent.
|
||||||
|
|
||||||
|
An example may help. Assume replica count is 3 and perfectly matching
|
||||||
|
ROWIDs starting at 1.
|
||||||
|
|
||||||
|
First sync run, database has 6 rows:
|
||||||
|
|
||||||
|
* SyncPoint1 starts as -1.
|
||||||
|
* SyncPoint2 starts as -1.
|
||||||
|
* No rows between points, so no "all updates" rows.
|
||||||
|
* Six rows newer than SyncPoint1, so a third of the rows are sent
|
||||||
|
by node 1, another third by node 2, remaining third by node 3.
|
||||||
|
* SyncPoint1 is set as 6 (the newest ROWID known).
|
||||||
|
* SyncPoint2 is left as -1 since no "all updates" rows were synced.
|
||||||
|
|
||||||
|
Next sync run, database has 12 rows:
|
||||||
|
|
||||||
|
* SyncPoint1 starts as 6.
|
||||||
|
* SyncPoint2 starts as -1.
|
||||||
|
* The rows between -1 and 6 all trigger updates (most of which
|
||||||
|
should short-circuit on the remote end as having already been
|
||||||
|
done).
|
||||||
|
* Six more rows newer than SyncPoint1, so a third of the rows are
|
||||||
|
sent by node 1, another third by node 2, remaining third by node
|
||||||
|
3.
|
||||||
|
* SyncPoint1 is set as 12 (the newest ROWID known).
|
||||||
|
* SyncPoint2 is set as 6 (the newest "all updates" ROWID).
|
||||||
|
|
||||||
|
In this way, under normal circumstances each node sends its share of
|
||||||
|
updates each run and just sends a batch of older updates to ensure nothing
|
||||||
|
was missed.
|
|
@ -13,9 +13,9 @@ special manifest file is created that, when downloaded, sends all the segments
|
||||||
concatenated as a single object. This also offers much greater upload speed
|
concatenated as a single object. This also offers much greater upload speed
|
||||||
with the possibility of parallel uploads of the segments.
|
with the possibility of parallel uploads of the segments.
|
||||||
|
|
||||||
----------------------------------
|
-------------------------------------
|
||||||
Using ``swift`` for Segmented Objects
|
Using ``swift`` for Segmented Objects
|
||||||
----------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
The quickest way to try out this feature is use the included ``swift`` Swift Tool.
|
The quickest way to try out this feature is use the included ``swift`` Swift Tool.
|
||||||
You can use the ``-S`` option to specify the segment size to use when splitting
|
You can use the ``-S`` option to specify the segment size to use when splitting
|
||||||
|
@ -120,6 +120,13 @@ Additional Notes
|
||||||
for the manifest itself, so this method was chosen to at least offer change
|
for the manifest itself, so this method was chosen to at least offer change
|
||||||
detection.
|
detection.
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you are using the container sync feature you will need to ensure both
|
||||||
|
your manifest file and your segment files are synced if they happen to be
|
||||||
|
in different containers.
|
||||||
|
|
||||||
-------
|
-------
|
||||||
History
|
History
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -35,20 +35,17 @@ rate_buffer_seconds 5 Number of seconds the rate counter can
|
||||||
faster than listed rate). A larger number
|
faster than listed rate). A larger number
|
||||||
will result in larger spikes in rate but
|
will result in larger spikes in rate but
|
||||||
better average accuracy.
|
better average accuracy.
|
||||||
account_ratelimit 0 If set, will limit all requests to
|
account_ratelimit 0 If set, will limit PUT and DELETE requests
|
||||||
/account_name and PUTs to
|
to /account_name/container_name.
|
||||||
/account_name/container_name. Number is in
|
Number is in requests per second.
|
||||||
requests per second
|
|
||||||
account_whitelist '' Comma separated lists of account names that
|
account_whitelist '' Comma separated lists of account names that
|
||||||
will not be rate limited.
|
will not be rate limited.
|
||||||
account_blacklist '' Comma separated lists of account names that
|
account_blacklist '' Comma separated lists of account names that
|
||||||
will not be allowed. Returns a 497 response.
|
will not be allowed. Returns a 497 response.
|
||||||
container_ratelimit_size '' When set with container_limit_x = r:
|
container_ratelimit_size '' When set with container_limit_x = r:
|
||||||
for containers of size x, limit requests
|
for containers of size x, limit requests
|
||||||
per second to r. Will limit GET and HEAD
|
per second to r. Will limit PUT, DELETE,
|
||||||
requests to /account_name/container_name
|
and POST requests to /a/c/o.
|
||||||
and PUTs and DELETEs to
|
|
||||||
/account_name/container_name/object_name
|
|
||||||
======================== ========= ===========================================
|
======================== ========= ===========================================
|
||||||
|
|
||||||
The container rate limits are linearly interpolated from the values given. A
|
The container rate limits are linearly interpolated from the values given. A
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
# swift_dir = /etc/swift
|
# swift_dir = /etc/swift
|
||||||
# devices = /srv/node
|
# devices = /srv/node
|
||||||
# mount_check = true
|
# mount_check = true
|
||||||
|
# This is a comma separated list of hosts allowed in the X-Container-Sync-To
|
||||||
|
# field for containers.
|
||||||
|
# allowed_sync_hosts = 127.0.0.1
|
||||||
# You can specify default log routing here if you want:
|
# You can specify default log routing here if you want:
|
||||||
# log_name = swift
|
# log_name = swift
|
||||||
# log_facility = LOG_LOCAL0
|
# log_facility = LOG_LOCAL0
|
||||||
|
@ -60,3 +63,15 @@ use = egg:swift#container
|
||||||
# log_level = INFO
|
# log_level = INFO
|
||||||
# Will audit, at most, 1 container per device per interval
|
# Will audit, at most, 1 container per device per interval
|
||||||
# interval = 1800
|
# interval = 1800
|
||||||
|
|
||||||
|
[container-sync]
|
||||||
|
# You can override the default log routing for this app here (don't use set!):
|
||||||
|
# log_name = container-sync
|
||||||
|
# log_facility = LOG_LOCAL0
|
||||||
|
# log_level = INFO
|
||||||
|
# If you need to use an HTTP Proxy, set it here; defaults to no proxy.
|
||||||
|
# sync_proxy = http://127.0.0.1:8888
|
||||||
|
# Will sync, at most, each container once per interval
|
||||||
|
# interval = 300
|
||||||
|
# Maximum amount of time to spend syncing each container per pass
|
||||||
|
# container_time = 60
|
||||||
|
|
|
@ -65,6 +65,9 @@ use = egg:swift#tempauth
|
||||||
# to the auth subsystem, for granting tokens, etc.
|
# to the auth subsystem, for granting tokens, etc.
|
||||||
# auth_prefix = /auth/
|
# auth_prefix = /auth/
|
||||||
# token_life = 86400
|
# token_life = 86400
|
||||||
|
# This is a comma separated list of hosts allowed to send X-Container-Sync-Key
|
||||||
|
# requests.
|
||||||
|
# allowed_sync_hosts = 127.0.0.1
|
||||||
# Lastly, you need to list all the accounts/users you want here. The format is:
|
# Lastly, you need to list all the accounts/users you want here. The format is:
|
||||||
# user_<account>_<user> = <key> [group] [group] [...] [storage_url]
|
# user_<account>_<user> = <key> [group] [group] [...] [storage_url]
|
||||||
# There are special groups of:
|
# There are special groups of:
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -80,7 +80,7 @@ setup(
|
||||||
'bin/swift-account-audit', 'bin/swift-account-reaper',
|
'bin/swift-account-audit', 'bin/swift-account-reaper',
|
||||||
'bin/swift-account-replicator', 'bin/swift-account-server',
|
'bin/swift-account-replicator', 'bin/swift-account-server',
|
||||||
'bin/swift-container-auditor',
|
'bin/swift-container-auditor',
|
||||||
'bin/swift-container-replicator',
|
'bin/swift-container-replicator', 'bin/swift-container-sync',
|
||||||
'bin/swift-container-server', 'bin/swift-container-updater',
|
'bin/swift-container-server', 'bin/swift-container-updater',
|
||||||
'bin/swift-drive-audit', 'bin/swift-get-nodes',
|
'bin/swift-drive-audit', 'bin/swift-get-nodes',
|
||||||
'bin/swift-init', 'bin/swift-object-auditor',
|
'bin/swift-init', 'bin/swift-object-auditor',
|
||||||
|
|
|
@ -137,22 +137,27 @@ class ClientException(Exception):
|
||||||
return b and '%s: %s' % (a, b) or a
|
return b and '%s: %s' % (a, b) or a
|
||||||
|
|
||||||
|
|
||||||
def http_connection(url):
|
def http_connection(url, proxy=None):
|
||||||
"""
|
"""
|
||||||
Make an HTTPConnection or HTTPSConnection
|
Make an HTTPConnection or HTTPSConnection
|
||||||
|
|
||||||
:param url: url to connect to
|
:param url: url to connect to
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:returns: tuple of (parsed url, connection object)
|
:returns: tuple of (parsed url, connection object)
|
||||||
:raises ClientException: Unable to handle protocol scheme
|
:raises ClientException: Unable to handle protocol scheme
|
||||||
"""
|
"""
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
|
proxy_parsed = urlparse(proxy) if proxy else None
|
||||||
if parsed.scheme == 'http':
|
if parsed.scheme == 'http':
|
||||||
conn = HTTPConnection(parsed.netloc)
|
conn = HTTPConnection((proxy_parsed if proxy else parsed).netloc)
|
||||||
elif parsed.scheme == 'https':
|
elif parsed.scheme == 'https':
|
||||||
conn = HTTPSConnection(parsed.netloc)
|
conn = HTTPSConnection((proxy_parsed if proxy else parsed).netloc)
|
||||||
else:
|
else:
|
||||||
raise ClientException('Cannot handle protocol scheme %s for url %s' %
|
raise ClientException('Cannot handle protocol scheme %s for url %s' %
|
||||||
(parsed.scheme, repr(url)))
|
(parsed.scheme, repr(url)))
|
||||||
|
if proxy:
|
||||||
|
conn._set_tunnel(parsed.hostname, parsed.port)
|
||||||
return parsed, conn
|
return parsed, conn
|
||||||
|
|
||||||
|
|
||||||
|
@ -565,40 +570,60 @@ def head_object(url, token, container, name, http_conn=None):
|
||||||
return resp_headers
|
return resp_headers
|
||||||
|
|
||||||
|
|
||||||
def put_object(url, token, container, name, contents, content_length=None,
|
def put_object(url, token=None, container=None, name=None, contents=None,
|
||||||
etag=None, chunk_size=65536, content_type=None, headers=None,
|
content_length=None, etag=None, chunk_size=65536,
|
||||||
http_conn=None):
|
content_type=None, headers=None, http_conn=None, proxy=None):
|
||||||
"""
|
"""
|
||||||
Put an object
|
Put an object
|
||||||
|
|
||||||
:param url: storage URL
|
:param url: storage URL
|
||||||
:param token: auth token
|
:param token: auth token; if None, no token will be sent
|
||||||
:param container: container name that the object is in
|
:param container: container name that the object is in; if None, the
|
||||||
:param name: object name to put
|
container name is expected to be part of the url
|
||||||
:param contents: a string or a file like object to read object data from
|
:param name: object name to put; if None, the object name is expected to be
|
||||||
|
part of the url
|
||||||
|
:param contents: a string or a file like object to read object data from;
|
||||||
|
if None, a zero-byte put will be done
|
||||||
:param content_length: value to send as content-length header; also limits
|
:param content_length: value to send as content-length header; also limits
|
||||||
the amount read from contents
|
the amount read from contents; if None, it will be
|
||||||
:param etag: etag of contents
|
computed via the contents or chunked transfer
|
||||||
:param chunk_size: chunk size of data to write
|
encoding will be used
|
||||||
:param content_type: value to send as content-type header
|
:param etag: etag of contents; if None, no etag will be sent
|
||||||
:param headers: additional headers to include in the request
|
:param chunk_size: chunk size of data to write; default 65536
|
||||||
|
:param content_type: value to send as content-type header; if None, no
|
||||||
|
content-type will be set (remote end will likely try
|
||||||
|
to auto-detect it)
|
||||||
|
:param headers: additional headers to include in the request, if any
|
||||||
:param http_conn: HTTP connection object (If None, it will create the
|
:param http_conn: HTTP connection object (If None, it will create the
|
||||||
conn object)
|
conn object)
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:returns: etag from server response
|
:returns: etag from server response
|
||||||
:raises ClientException: HTTP PUT request failed
|
:raises ClientException: HTTP PUT request failed
|
||||||
"""
|
"""
|
||||||
if http_conn:
|
if http_conn:
|
||||||
parsed, conn = http_conn
|
parsed, conn = http_conn
|
||||||
else:
|
else:
|
||||||
parsed, conn = http_connection(url)
|
parsed, conn = http_connection(url, proxy=proxy)
|
||||||
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
path = parsed.path
|
||||||
if not headers:
|
if container:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
||||||
|
if name:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
||||||
|
if headers:
|
||||||
|
headers = dict(headers)
|
||||||
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['X-Auth-Token'] = token
|
if token:
|
||||||
|
headers['X-Auth-Token'] = token
|
||||||
if etag:
|
if etag:
|
||||||
headers['ETag'] = etag.strip('"')
|
headers['ETag'] = etag.strip('"')
|
||||||
if content_length is not None:
|
if content_length is not None:
|
||||||
headers['Content-Length'] = str(content_length)
|
headers['Content-Length'] = str(content_length)
|
||||||
|
else:
|
||||||
|
for n, v in headers.iteritems():
|
||||||
|
if n.lower() == 'content-length':
|
||||||
|
content_length = int(v)
|
||||||
if content_type is not None:
|
if content_type is not None:
|
||||||
headers['Content-Type'] = content_type
|
headers['Content-Type'] = content_type
|
||||||
if not contents:
|
if not contents:
|
||||||
|
@ -633,7 +658,7 @@ def put_object(url, token, container, name, contents, content_length=None,
|
||||||
raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
|
raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
|
||||||
http_host=conn.host, http_port=conn.port, http_path=path,
|
http_host=conn.host, http_port=conn.port, http_path=path,
|
||||||
http_status=resp.status, http_reason=resp.reason)
|
http_status=resp.status, http_reason=resp.reason)
|
||||||
return resp.getheader('etag').strip('"')
|
return resp.getheader('etag', '').strip('"')
|
||||||
|
|
||||||
|
|
||||||
def post_object(url, token, container, name, headers, http_conn=None):
|
def post_object(url, token, container, name, headers, http_conn=None):
|
||||||
|
@ -664,24 +689,40 @@ def post_object(url, token, container, name, headers, http_conn=None):
|
||||||
http_status=resp.status, http_reason=resp.reason)
|
http_status=resp.status, http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def delete_object(url, token, container, name, http_conn=None):
|
def delete_object(url, token=None, container=None, name=None, http_conn=None,
|
||||||
|
headers=None, proxy=None):
|
||||||
"""
|
"""
|
||||||
Delete object
|
Delete object
|
||||||
|
|
||||||
:param url: storage URL
|
:param url: storage URL
|
||||||
:param token: auth token
|
:param token: auth token; if None, no token will be sent
|
||||||
:param container: container name that the object is in
|
:param container: container name that the object is in; if None, the
|
||||||
:param name: object name to delete
|
container name is expected to be part of the url
|
||||||
|
:param name: object name to delete; if None, the object name is expected to
|
||||||
|
be part of the url
|
||||||
:param http_conn: HTTP connection object (If None, it will create the
|
:param http_conn: HTTP connection object (If None, it will create the
|
||||||
conn object)
|
conn object)
|
||||||
|
:param headers: additional headers to include in the request
|
||||||
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
||||||
|
format 'http://127.0.0.1:8888' to set one
|
||||||
:raises ClientException: HTTP DELETE request failed
|
:raises ClientException: HTTP DELETE request failed
|
||||||
"""
|
"""
|
||||||
if http_conn:
|
if http_conn:
|
||||||
parsed, conn = http_conn
|
parsed, conn = http_conn
|
||||||
else:
|
else:
|
||||||
parsed, conn = http_connection(url)
|
parsed, conn = http_connection(url, proxy=proxy)
|
||||||
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
path = parsed.path
|
||||||
conn.request('DELETE', path, '', {'X-Auth-Token': token})
|
if container:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
||||||
|
if name:
|
||||||
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
||||||
|
if headers:
|
||||||
|
headers = dict(headers)
|
||||||
|
else:
|
||||||
|
headers = {}
|
||||||
|
if token:
|
||||||
|
headers['X-Auth-Token'] = token
|
||||||
|
conn.request('DELETE', path, '', headers)
|
||||||
resp = conn.getresponse()
|
resp = conn.getresponse()
|
||||||
resp.read()
|
resp.read()
|
||||||
if resp.status < 200 or resp.status >= 300:
|
if resp.status < 200 or resp.status >= 300:
|
||||||
|
|
|
@ -666,7 +666,9 @@ class ContainerBroker(DatabaseBroker):
|
||||||
id TEXT,
|
id TEXT,
|
||||||
status TEXT DEFAULT '',
|
status TEXT DEFAULT '',
|
||||||
status_changed_at TEXT DEFAULT '0',
|
status_changed_at TEXT DEFAULT '0',
|
||||||
metadata TEXT DEFAULT ''
|
metadata TEXT DEFAULT '',
|
||||||
|
x_container_sync_point1 INTEGER DEFAULT -1,
|
||||||
|
x_container_sync_point2 INTEGER DEFAULT -1
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO container_stat (object_count, bytes_used)
|
INSERT INTO container_stat (object_count, bytes_used)
|
||||||
|
@ -886,7 +888,8 @@ class ContainerBroker(DatabaseBroker):
|
||||||
:returns: dict with keys: account, container, created_at,
|
:returns: dict with keys: account, container, created_at,
|
||||||
put_timestamp, delete_timestamp, object_count, bytes_used,
|
put_timestamp, delete_timestamp, object_count, bytes_used,
|
||||||
reported_put_timestamp, reported_delete_timestamp,
|
reported_put_timestamp, reported_delete_timestamp,
|
||||||
reported_object_count, reported_bytes_used, hash, id
|
reported_object_count, reported_bytes_used, hash, id,
|
||||||
|
x_container_sync_point1, and x_container_sync_point2.
|
||||||
If include_metadata is set, metadata is included as a key
|
If include_metadata is set, metadata is included as a key
|
||||||
pointing to a dict of tuples of the metadata
|
pointing to a dict of tuples of the metadata
|
||||||
"""
|
"""
|
||||||
|
@ -896,35 +899,83 @@ class ContainerBroker(DatabaseBroker):
|
||||||
if not self.stale_reads_ok:
|
if not self.stale_reads_ok:
|
||||||
raise
|
raise
|
||||||
with self.get() as conn:
|
with self.get() as conn:
|
||||||
metadata = ''
|
data = None
|
||||||
if include_metadata:
|
trailing1 = 'metadata'
|
||||||
metadata = ', metadata'
|
trailing2 = 'x_container_sync_point1, x_container_sync_point2'
|
||||||
try:
|
while not data:
|
||||||
data = conn.execute('''
|
try:
|
||||||
SELECT account, container, created_at, put_timestamp,
|
data = conn.execute('''
|
||||||
delete_timestamp, object_count, bytes_used,
|
SELECT account, container, created_at, put_timestamp,
|
||||||
reported_put_timestamp, reported_delete_timestamp,
|
delete_timestamp, object_count, bytes_used,
|
||||||
reported_object_count, reported_bytes_used, hash, id
|
reported_put_timestamp, reported_delete_timestamp,
|
||||||
%s
|
reported_object_count, reported_bytes_used, hash,
|
||||||
FROM container_stat
|
id, %s, %s
|
||||||
''' % metadata).fetchone()
|
FROM container_stat
|
||||||
except sqlite3.OperationalError, err:
|
''' % (trailing1, trailing2)).fetchone()
|
||||||
if 'no such column: metadata' not in str(err):
|
except sqlite3.OperationalError, err:
|
||||||
raise
|
if 'no such column: metadata' in str(err):
|
||||||
data = conn.execute('''
|
trailing1 = "'' as metadata"
|
||||||
SELECT account, container, created_at, put_timestamp,
|
elif 'no such column: x_container_sync_point' in str(err):
|
||||||
delete_timestamp, object_count, bytes_used,
|
trailing2 = '-1 AS x_container_sync_point1, ' \
|
||||||
reported_put_timestamp, reported_delete_timestamp,
|
'-1 AS x_container_sync_point2'
|
||||||
reported_object_count, reported_bytes_used, hash, id
|
else:
|
||||||
FROM container_stat''').fetchone()
|
raise
|
||||||
data = dict(data)
|
data = dict(data)
|
||||||
if include_metadata:
|
if include_metadata:
|
||||||
try:
|
try:
|
||||||
data['metadata'] = json.loads(data.get('metadata', ''))
|
data['metadata'] = json.loads(data.get('metadata', ''))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
data['metadata'] = {}
|
data['metadata'] = {}
|
||||||
|
elif 'metadata' in data:
|
||||||
|
del data['metadata']
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def set_x_container_sync_points(self, sync_point1, sync_point2):
|
||||||
|
with self.get() as conn:
|
||||||
|
orig_isolation_level = conn.isolation_level
|
||||||
|
try:
|
||||||
|
# We turn off auto-transactions to ensure the alter table
|
||||||
|
# commands are part of the transaction.
|
||||||
|
conn.isolation_level = None
|
||||||
|
conn.execute('BEGIN')
|
||||||
|
try:
|
||||||
|
self._set_x_container_sync_points(conn, sync_point1,
|
||||||
|
sync_point2)
|
||||||
|
except sqlite3.OperationalError, err:
|
||||||
|
if 'no such column: x_container_sync_point' not in str(err):
|
||||||
|
raise
|
||||||
|
conn.execute('''
|
||||||
|
ALTER TABLE container_stat
|
||||||
|
ADD COLUMN x_container_sync_point1 INTEGER DEFAULT -1
|
||||||
|
''')
|
||||||
|
conn.execute('''
|
||||||
|
ALTER TABLE container_stat
|
||||||
|
ADD COLUMN x_container_sync_point2 INTEGER DEFAULT -1
|
||||||
|
''')
|
||||||
|
self._set_x_container_sync_points(conn, sync_point1,
|
||||||
|
sync_point2)
|
||||||
|
conn.execute('COMMIT')
|
||||||
|
finally:
|
||||||
|
conn.isolation_level = orig_isolation_level
|
||||||
|
|
||||||
|
def _set_x_container_sync_points(self, conn, sync_point1, sync_point2):
|
||||||
|
if sync_point1 is not None and sync_point2 is not None:
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE container_stat
|
||||||
|
SET x_container_sync_point1 = ?,
|
||||||
|
x_container_sync_point2 = ?
|
||||||
|
''', (sync_point1, sync_point2))
|
||||||
|
elif sync_point1 is not None:
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE container_stat
|
||||||
|
SET x_container_sync_point1 = ?
|
||||||
|
''', (sync_point1,))
|
||||||
|
elif sync_point2 is not None:
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE container_stat
|
||||||
|
SET x_container_sync_point2 = ?
|
||||||
|
''', (sync_point2,))
|
||||||
|
|
||||||
def reported(self, put_timestamp, delete_timestamp, object_count,
|
def reported(self, put_timestamp, delete_timestamp, object_count,
|
||||||
bytes_used):
|
bytes_used):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -31,9 +31,10 @@ RUN_DIR = '/var/run/swift'
|
||||||
|
|
||||||
# auth-server has been removed from ALL_SERVERS, start it explicitly
|
# auth-server has been removed from ALL_SERVERS, start it explicitly
|
||||||
ALL_SERVERS = ['account-auditor', 'account-server', 'container-auditor',
|
ALL_SERVERS = ['account-auditor', 'account-server', 'container-auditor',
|
||||||
'container-replicator', 'container-server', 'container-updater',
|
'container-replicator', 'container-server', 'container-sync',
|
||||||
'object-auditor', 'object-server', 'object-replicator', 'object-updater',
|
'container-updater', 'object-auditor', 'object-server',
|
||||||
'proxy-server', 'account-replicator', 'account-reaper']
|
'object-replicator', 'object-updater', 'proxy-server',
|
||||||
|
'account-replicator', 'account-reaper']
|
||||||
MAIN_SERVERS = ['proxy-server', 'account-server', 'container-server',
|
MAIN_SERVERS = ['proxy-server', 'account-server', 'container-server',
|
||||||
'object-server']
|
'object-server']
|
||||||
REST_SERVERS = [s for s in ALL_SERVERS if s not in MAIN_SERVERS]
|
REST_SERVERS = [s for s in ALL_SERVERS if s not in MAIN_SERVERS]
|
||||||
|
|
|
@ -45,6 +45,10 @@ class DomainRemapMiddleware(object):
|
||||||
derive account and container names from elements in the domain name and
|
derive account and container names from elements in the domain name and
|
||||||
put those derived values into the URL path (leaving the Host header
|
put those derived values into the URL path (leaving the Host header
|
||||||
unchanged).
|
unchanged).
|
||||||
|
|
||||||
|
Also note that using container sync with remapped domain names is not
|
||||||
|
advised. With container sync, you should use the true storage end points as
|
||||||
|
sync destinations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app, conf):
|
def __init__(self, app, conf):
|
||||||
|
|
|
@ -105,16 +105,15 @@ class RateLimitMiddleware(object):
|
||||||
:param obj_name: object name from path
|
:param obj_name: object name from path
|
||||||
"""
|
"""
|
||||||
keys = []
|
keys = []
|
||||||
if self.account_ratelimit and account_name and (
|
# COPYs are not limited
|
||||||
not (container_name or obj_name) or
|
if self.account_ratelimit and \
|
||||||
(container_name and not obj_name and
|
account_name and container_name and not obj_name and \
|
||||||
req_method in ('PUT', 'DELETE'))):
|
req_method in ('PUT', 'DELETE'):
|
||||||
keys.append(("ratelimit/%s" % account_name,
|
keys.append(("ratelimit/%s" % account_name,
|
||||||
self.account_ratelimit))
|
self.account_ratelimit))
|
||||||
|
|
||||||
if account_name and container_name and (
|
if account_name and container_name and obj_name and \
|
||||||
(not obj_name and req_method in ('GET', 'HEAD')) or
|
req_method in ('PUT', 'DELETE', 'POST'):
|
||||||
(obj_name and req_method in ('PUT', 'DELETE'))):
|
|
||||||
container_size = None
|
container_size = None
|
||||||
memcache_key = get_container_memcache_key(account_name,
|
memcache_key = get_container_memcache_key(account_name,
|
||||||
container_name)
|
container_name)
|
||||||
|
|
|
@ -128,13 +128,23 @@ def canonical_string(req):
|
||||||
"""
|
"""
|
||||||
Canonicalize a request to a token that can be signed.
|
Canonicalize a request to a token that can be signed.
|
||||||
"""
|
"""
|
||||||
|
amz_headers = {}
|
||||||
|
|
||||||
buf = "%s\n%s\n%s\n" % (req.method, req.headers.get('Content-MD5', ''),
|
buf = "%s\n%s\n%s\n" % (req.method, req.headers.get('Content-MD5', ''),
|
||||||
req.headers.get('Content-Type') or '')
|
req.headers.get('Content-Type') or '')
|
||||||
if 'Date' in req.headers:
|
|
||||||
buf += "%s\n" % req.headers['Date']
|
|
||||||
for amz_header in sorted((key.lower() for key in req.headers
|
for amz_header in sorted((key.lower() for key in req.headers
|
||||||
if key.lower().startswith('x-amz-'))):
|
if key.lower().startswith('x-amz-'))):
|
||||||
buf += "%s:%s\n" % (amz_header, req.headers[amz_header])
|
amz_headers[amz_header] = req.headers[amz_header]
|
||||||
|
|
||||||
|
if 'x-amz-date' in amz_headers:
|
||||||
|
buf += "\n"
|
||||||
|
elif 'Date' in req.headers:
|
||||||
|
buf += "%s\n" % req.headers['Date']
|
||||||
|
|
||||||
|
for k in sorted(key.lower() for key in amz_headers):
|
||||||
|
buf += "%s:%s\n" % (k, amz_headers[k])
|
||||||
|
|
||||||
path = req.path_qs
|
path = req.path_qs
|
||||||
if '?' in path:
|
if '?' in path:
|
||||||
path, args = path.split('?', 1)
|
path, args = path.split('?', 1)
|
||||||
|
|
|
@ -27,7 +27,8 @@ from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
|
||||||
HTTPUnauthorized
|
HTTPUnauthorized
|
||||||
|
|
||||||
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
|
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
|
||||||
from swift.common.utils import cache_from_env, get_logger, split_path
|
from swift.common.utils import cache_from_env, get_logger, get_remote_client, \
|
||||||
|
split_path
|
||||||
|
|
||||||
|
|
||||||
class TempAuth(object):
|
class TempAuth(object):
|
||||||
|
@ -70,6 +71,9 @@ class TempAuth(object):
|
||||||
if self.auth_prefix[-1] != '/':
|
if self.auth_prefix[-1] != '/':
|
||||||
self.auth_prefix += '/'
|
self.auth_prefix += '/'
|
||||||
self.token_life = int(conf.get('token_life', 86400))
|
self.token_life = int(conf.get('token_life', 86400))
|
||||||
|
self.allowed_sync_hosts = [h.strip()
|
||||||
|
for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
|
||||||
|
if h.strip()]
|
||||||
self.users = {}
|
self.users = {}
|
||||||
for conf_key in conf:
|
for conf_key in conf:
|
||||||
if conf_key.startswith('user_'):
|
if conf_key.startswith('user_'):
|
||||||
|
@ -245,11 +249,20 @@ class TempAuth(object):
|
||||||
if '.reseller_admin' in user_groups and \
|
if '.reseller_admin' in user_groups and \
|
||||||
account != self.reseller_prefix and \
|
account != self.reseller_prefix and \
|
||||||
account[len(self.reseller_prefix)] != '.':
|
account[len(self.reseller_prefix)] != '.':
|
||||||
|
req.environ['swift_owner'] = True
|
||||||
return None
|
return None
|
||||||
if account in user_groups and \
|
if account in user_groups and \
|
||||||
(req.method not in ('DELETE', 'PUT') or container):
|
(req.method not in ('DELETE', 'PUT') or container):
|
||||||
# If the user is admin for the account and is not trying to do an
|
# If the user is admin for the account and is not trying to do an
|
||||||
# account DELETE or PUT...
|
# account DELETE or PUT...
|
||||||
|
req.environ['swift_owner'] = True
|
||||||
|
return None
|
||||||
|
if (req.environ.get('swift_sync_key') and
|
||||||
|
req.environ['swift_sync_key'] ==
|
||||||
|
req.headers.get('x-container-sync-key', None) and
|
||||||
|
'x-timestamp' in req.headers and
|
||||||
|
(req.remote_addr in self.allowed_sync_hosts or
|
||||||
|
get_remote_client(req) in self.allowed_sync_hosts)):
|
||||||
return None
|
return None
|
||||||
referrers, groups = parse_acl(getattr(req, 'acl', None))
|
referrers, groups = parse_acl(getattr(req, 'acl', None))
|
||||||
if referrer_allowed(req.referer, referrers):
|
if referrer_allowed(req.referer, referrers):
|
||||||
|
|
|
@ -972,6 +972,32 @@ def urlparse(url):
|
||||||
return ModifiedParseResult(*stdlib_urlparse(url))
|
return ModifiedParseResult(*stdlib_urlparse(url))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sync_to(value, allowed_sync_hosts):
|
||||||
|
p = urlparse(value)
|
||||||
|
if p.scheme not in ('http', 'https'):
|
||||||
|
return _('Invalid scheme %r in X-Container-Sync-To, must be "http" '
|
||||||
|
'or "https".') % p.scheme
|
||||||
|
if not p.path:
|
||||||
|
return _('Path required in X-Container-Sync-To')
|
||||||
|
if p.params or p.query or p.fragment:
|
||||||
|
return _('Params, queries, and fragments not allowed in '
|
||||||
|
'X-Container-Sync-To')
|
||||||
|
if p.hostname not in allowed_sync_hosts:
|
||||||
|
return _('Invalid host %r in X-Container-Sync-To') % p.hostname
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_client(req):
|
||||||
|
# remote host for zeus
|
||||||
|
client = req.headers.get('x-cluster-client-ip')
|
||||||
|
if not client and 'x-forwarded-for' in req.headers:
|
||||||
|
# remote host for other lbs
|
||||||
|
client = req.headers['x-forwarded-for'].split(',')[0].strip()
|
||||||
|
if not client:
|
||||||
|
client = req.remote_addr
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
def human_readable(value):
|
def human_readable(value):
|
||||||
"""
|
"""
|
||||||
Returns the number in a human readable format; for example 1048576 = "1Mi".
|
Returns the number in a human readable format; for example 1048576 = "1Mi".
|
||||||
|
|
|
@ -32,7 +32,7 @@ from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
|
||||||
|
|
||||||
from swift.common.db import ContainerBroker
|
from swift.common.db import ContainerBroker
|
||||||
from swift.common.utils import get_logger, get_param, hash_path, \
|
from swift.common.utils import get_logger, get_param, hash_path, \
|
||||||
normalize_timestamp, storage_directory, split_path
|
normalize_timestamp, storage_directory, split_path, validate_sync_to
|
||||||
from swift.common.constraints import CONTAINER_LISTING_LIMIT, \
|
from swift.common.constraints import CONTAINER_LISTING_LIMIT, \
|
||||||
check_mount, check_float, check_utf8
|
check_mount, check_float, check_utf8
|
||||||
from swift.common.bufferedhttp import http_connect
|
from swift.common.bufferedhttp import http_connect
|
||||||
|
@ -46,7 +46,8 @@ class ContainerController(object):
|
||||||
"""WSGI Controller for the container server."""
|
"""WSGI Controller for the container server."""
|
||||||
|
|
||||||
# Ensure these are all lowercase
|
# Ensure these are all lowercase
|
||||||
save_headers = ['x-container-read', 'x-container-write']
|
save_headers = ['x-container-read', 'x-container-write',
|
||||||
|
'x-container-sync-key', 'x-container-sync-to']
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
self.logger = get_logger(conf, log_route='container-server')
|
self.logger = get_logger(conf, log_route='container-server')
|
||||||
|
@ -55,6 +56,9 @@ class ContainerController(object):
|
||||||
('true', 't', '1', 'on', 'yes', 'y')
|
('true', 't', '1', 'on', 'yes', 'y')
|
||||||
self.node_timeout = int(conf.get('node_timeout', 3))
|
self.node_timeout = int(conf.get('node_timeout', 3))
|
||||||
self.conn_timeout = float(conf.get('conn_timeout', 0.5))
|
self.conn_timeout = float(conf.get('conn_timeout', 0.5))
|
||||||
|
self.allowed_sync_hosts = [h.strip()
|
||||||
|
for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
|
||||||
|
if h.strip()]
|
||||||
self.replicator_rpc = ReplicatorRpc(self.root, DATADIR,
|
self.replicator_rpc = ReplicatorRpc(self.root, DATADIR,
|
||||||
ContainerBroker, self.mount_check, logger=self.logger)
|
ContainerBroker, self.mount_check, logger=self.logger)
|
||||||
|
|
||||||
|
@ -174,6 +178,11 @@ class ContainerController(object):
|
||||||
not check_float(req.headers['x-timestamp']):
|
not check_float(req.headers['x-timestamp']):
|
||||||
return HTTPBadRequest(body='Missing timestamp', request=req,
|
return HTTPBadRequest(body='Missing timestamp', request=req,
|
||||||
content_type='text/plain')
|
content_type='text/plain')
|
||||||
|
if 'x-container-sync-to' in req.headers:
|
||||||
|
err = validate_sync_to(req.headers['x-container-sync-to'],
|
||||||
|
self.allowed_sync_hosts)
|
||||||
|
if err:
|
||||||
|
return HTTPBadRequest(err)
|
||||||
if self.mount_check and not check_mount(self.root, drive):
|
if self.mount_check and not check_mount(self.root, drive):
|
||||||
return Response(status='507 %s is not mounted' % drive)
|
return Response(status='507 %s is not mounted' % drive)
|
||||||
timestamp = normalize_timestamp(req.headers['x-timestamp'])
|
timestamp = normalize_timestamp(req.headers['x-timestamp'])
|
||||||
|
@ -199,6 +208,11 @@ class ContainerController(object):
|
||||||
if key.lower() in self.save_headers or
|
if key.lower() in self.save_headers or
|
||||||
key.lower().startswith('x-container-meta-'))
|
key.lower().startswith('x-container-meta-'))
|
||||||
if metadata:
|
if metadata:
|
||||||
|
if 'X-Container-Sync-To' in metadata:
|
||||||
|
if 'X-Container-Sync-To' not in broker.metadata or \
|
||||||
|
metadata['X-Container-Sync-To'][0] != \
|
||||||
|
broker.metadata['X-Container-Sync-To'][0]:
|
||||||
|
broker.set_x_container_sync_points(-1, -1)
|
||||||
broker.update_metadata(metadata)
|
broker.update_metadata(metadata)
|
||||||
resp = self.account_update(req, account, container, broker)
|
resp = self.account_update(req, account, container, broker)
|
||||||
if resp:
|
if resp:
|
||||||
|
@ -232,7 +246,8 @@ class ContainerController(object):
|
||||||
}
|
}
|
||||||
headers.update((key, value)
|
headers.update((key, value)
|
||||||
for key, (value, timestamp) in broker.metadata.iteritems()
|
for key, (value, timestamp) in broker.metadata.iteritems()
|
||||||
if value != '')
|
if value != '' and (key.lower() in self.save_headers or
|
||||||
|
key.lower().startswith('x-container-meta-')))
|
||||||
return HTTPNoContent(request=req, headers=headers)
|
return HTTPNoContent(request=req, headers=headers)
|
||||||
|
|
||||||
def GET(self, req):
|
def GET(self, req):
|
||||||
|
@ -259,7 +274,8 @@ class ContainerController(object):
|
||||||
}
|
}
|
||||||
resp_headers.update((key, value)
|
resp_headers.update((key, value)
|
||||||
for key, (value, timestamp) in broker.metadata.iteritems()
|
for key, (value, timestamp) in broker.metadata.iteritems()
|
||||||
if value != '')
|
if value != '' and (key.lower() in self.save_headers or
|
||||||
|
key.lower().startswith('x-container-meta-')))
|
||||||
try:
|
try:
|
||||||
path = get_param(req, 'path')
|
path = get_param(req, 'path')
|
||||||
prefix = get_param(req, 'prefix')
|
prefix = get_param(req, 'prefix')
|
||||||
|
@ -368,6 +384,11 @@ class ContainerController(object):
|
||||||
not check_float(req.headers['x-timestamp']):
|
not check_float(req.headers['x-timestamp']):
|
||||||
return HTTPBadRequest(body='Missing or bad timestamp',
|
return HTTPBadRequest(body='Missing or bad timestamp',
|
||||||
request=req, content_type='text/plain')
|
request=req, content_type='text/plain')
|
||||||
|
if 'x-container-sync-to' in req.headers:
|
||||||
|
err = validate_sync_to(req.headers['x-container-sync-to'],
|
||||||
|
self.allowed_sync_hosts)
|
||||||
|
if err:
|
||||||
|
return HTTPBadRequest(err)
|
||||||
if self.mount_check and not check_mount(self.root, drive):
|
if self.mount_check and not check_mount(self.root, drive):
|
||||||
return Response(status='507 %s is not mounted' % drive)
|
return Response(status='507 %s is not mounted' % drive)
|
||||||
broker = self._get_container_broker(drive, part, account, container)
|
broker = self._get_container_broker(drive, part, account, container)
|
||||||
|
@ -380,6 +401,11 @@ class ContainerController(object):
|
||||||
if key.lower() in self.save_headers or
|
if key.lower() in self.save_headers or
|
||||||
key.lower().startswith('x-container-meta-'))
|
key.lower().startswith('x-container-meta-'))
|
||||||
if metadata:
|
if metadata:
|
||||||
|
if 'X-Container-Sync-To' in metadata:
|
||||||
|
if 'X-Container-Sync-To' not in broker.metadata or \
|
||||||
|
metadata['X-Container-Sync-To'][0] != \
|
||||||
|
broker.metadata['X-Container-Sync-To'][0]:
|
||||||
|
broker.set_x_container_sync_points(-1, -1)
|
||||||
broker.update_metadata(metadata)
|
broker.update_metadata(metadata)
|
||||||
return HTTPNoContent(request=req)
|
return HTTPNoContent(request=req)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,424 @@
|
||||||
|
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||||
|
#
|
||||||
|
# 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 os
|
||||||
|
from time import ctime, time
|
||||||
|
from random import random, shuffle
|
||||||
|
from struct import unpack_from
|
||||||
|
|
||||||
|
from eventlet import sleep
|
||||||
|
|
||||||
|
from swift.container import server as container_server
|
||||||
|
from swift.common.client import ClientException, delete_object, put_object, \
|
||||||
|
quote
|
||||||
|
from swift.common.direct_client import direct_get_object
|
||||||
|
from swift.common.ring import Ring
|
||||||
|
from swift.common.db import ContainerBroker
|
||||||
|
from swift.common.utils import audit_location_generator, get_logger, \
|
||||||
|
hash_path, normalize_timestamp, TRUE_VALUES, validate_sync_to, whataremyips
|
||||||
|
from swift.common.daemon import Daemon
|
||||||
|
|
||||||
|
|
||||||
|
class _Iter2FileLikeObject(object):
|
||||||
|
"""
|
||||||
|
Returns an iterator's contents via :func:`read`, making it look like a file
|
||||||
|
object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iterator):
|
||||||
|
self.iterator = iterator
|
||||||
|
self._chunk = ''
|
||||||
|
|
||||||
|
def read(self, size=-1):
|
||||||
|
"""
|
||||||
|
read([size]) -> read at most size bytes, returned as a string.
|
||||||
|
|
||||||
|
If the size argument is negative or omitted, read until EOF is reached.
|
||||||
|
Notice that when in non-blocking mode, less data than what was
|
||||||
|
requested may be returned, even if no size parameter was given.
|
||||||
|
"""
|
||||||
|
if size < 0:
|
||||||
|
chunk = self._chunk
|
||||||
|
self._chunk = ''
|
||||||
|
return chunk + ''.join(self.iterator)
|
||||||
|
chunk = self._chunk
|
||||||
|
self._chunk = ''
|
||||||
|
if chunk and len(chunk) <= size:
|
||||||
|
return chunk
|
||||||
|
try:
|
||||||
|
chunk += self.iterator.next()
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
if len(chunk) <= size:
|
||||||
|
return chunk
|
||||||
|
self._chunk = chunk[size:]
|
||||||
|
return chunk[:size]
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerSync(Daemon):
|
||||||
|
"""
|
||||||
|
Daemon to sync syncable containers.
|
||||||
|
|
||||||
|
This is done by scanning the local devices for container databases and
|
||||||
|
checking for x-container-sync-to and x-container-sync-key metadata values.
|
||||||
|
If they exist, newer rows since the last sync will trigger PUTs or DELETEs
|
||||||
|
to the other container.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Container sync will sync object POSTs only if the proxy server is set
|
||||||
|
to use "object_post_as_copy = true" which is the default. So-called
|
||||||
|
fast object posts, "object_post_as_copy = false" do not update the
|
||||||
|
container listings and therefore can't be detected for synchronization.
|
||||||
|
|
||||||
|
The actual syncing is slightly more complicated to make use of the three
|
||||||
|
(or number-of-replicas) main nodes for a container without each trying to
|
||||||
|
do the exact same work but also without missing work if one node happens to
|
||||||
|
be down.
|
||||||
|
|
||||||
|
Two sync points are kept per container database. All rows between the two
|
||||||
|
sync points trigger updates. Any rows newer than both sync points cause
|
||||||
|
updates depending on the node's position for the container (primary nodes
|
||||||
|
do one third, etc. depending on the replica count of course). After a sync
|
||||||
|
run, the first sync point is set to the newest ROWID known and the second
|
||||||
|
sync point is set to newest ROWID for which all updates have been sent.
|
||||||
|
|
||||||
|
An example may help. Assume replica count is 3 and perfectly matching
|
||||||
|
ROWIDs starting at 1.
|
||||||
|
|
||||||
|
First sync run, database has 6 rows:
|
||||||
|
|
||||||
|
* SyncPoint1 starts as -1.
|
||||||
|
* SyncPoint2 starts as -1.
|
||||||
|
* No rows between points, so no "all updates" rows.
|
||||||
|
* Six rows newer than SyncPoint1, so a third of the rows are sent
|
||||||
|
by node 1, another third by node 2, remaining third by node 3.
|
||||||
|
* SyncPoint1 is set as 6 (the newest ROWID known).
|
||||||
|
* SyncPoint2 is left as -1 since no "all updates" rows were synced.
|
||||||
|
|
||||||
|
Next sync run, database has 12 rows:
|
||||||
|
|
||||||
|
* SyncPoint1 starts as 6.
|
||||||
|
* SyncPoint2 starts as -1.
|
||||||
|
* The rows between -1 and 6 all trigger updates (most of which
|
||||||
|
should short-circuit on the remote end as having already been
|
||||||
|
done).
|
||||||
|
* Six more rows newer than SyncPoint1, so a third of the rows are
|
||||||
|
sent by node 1, another third by node 2, remaining third by node
|
||||||
|
3.
|
||||||
|
* SyncPoint1 is set as 12 (the newest ROWID known).
|
||||||
|
* SyncPoint2 is set as 6 (the newest "all updates" ROWID).
|
||||||
|
|
||||||
|
In this way, under normal circumstances each node sends its share of
|
||||||
|
updates each run and just sends a batch of older updates to ensure nothing
|
||||||
|
was missed.
|
||||||
|
|
||||||
|
:param conf: The dict of configuration values from the [container-sync]
|
||||||
|
section of the container-server.conf
|
||||||
|
:param container_ring: If None, the <swift_dir>/container.ring.gz will be
|
||||||
|
loaded. This is overridden by unit tests.
|
||||||
|
:param object_ring: If None, the <swift_dir>/object.ring.gz will be loaded.
|
||||||
|
This is overridden by unit tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, conf, container_ring=None, object_ring=None):
|
||||||
|
#: The dict of configuration values from the [container-sync] section
|
||||||
|
#: of the container-server.conf.
|
||||||
|
self.conf = conf
|
||||||
|
#: Logger to use for container-sync log lines.
|
||||||
|
self.logger = get_logger(conf, log_route='container-sync')
|
||||||
|
#: Path to the local device mount points.
|
||||||
|
self.devices = conf.get('devices', '/srv/node')
|
||||||
|
#: Indicates whether mount points should be verified as actual mount
|
||||||
|
#: points (normally true, false for tests and SAIO).
|
||||||
|
self.mount_check = \
|
||||||
|
conf.get('mount_check', 'true').lower() in TRUE_VALUES
|
||||||
|
#: Minimum time between full scans. This is to keep the daemon from
|
||||||
|
#: running wild on near empty systems.
|
||||||
|
self.interval = int(conf.get('interval', 300))
|
||||||
|
#: Maximum amount of time to spend syncing a container before moving on
|
||||||
|
#: to the next one. If a conatiner sync hasn't finished in this time,
|
||||||
|
#: it'll just be resumed next scan.
|
||||||
|
self.container_time = int(conf.get('container_time', 60))
|
||||||
|
#: The list of hosts we're allowed to send syncs to.
|
||||||
|
self.allowed_sync_hosts = [h.strip()
|
||||||
|
for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
|
||||||
|
if h.strip()]
|
||||||
|
self.proxy = conf.get('sync_proxy')
|
||||||
|
#: Number of containers with sync turned on that were successfully
|
||||||
|
#: synced.
|
||||||
|
self.container_syncs = 0
|
||||||
|
#: Number of successful DELETEs triggered.
|
||||||
|
self.container_deletes = 0
|
||||||
|
#: Number of successful PUTs triggered.
|
||||||
|
self.container_puts = 0
|
||||||
|
#: Number of containers that didn't have sync turned on.
|
||||||
|
self.container_skips = 0
|
||||||
|
#: Number of containers that had a failure of some type.
|
||||||
|
self.container_failures = 0
|
||||||
|
#: Time of last stats report.
|
||||||
|
self.reported = time()
|
||||||
|
swift_dir = conf.get('swift_dir', '/etc/swift')
|
||||||
|
#: swift.common.ring.Ring for locating containers.
|
||||||
|
self.container_ring = container_ring or \
|
||||||
|
Ring(os.path.join(swift_dir, 'container.ring.gz'))
|
||||||
|
#: swift.common.ring.Ring for locating objects.
|
||||||
|
self.object_ring = object_ring or \
|
||||||
|
Ring(os.path.join(swift_dir, 'object.ring.gz'))
|
||||||
|
self._myips = whataremyips()
|
||||||
|
self._myport = int(conf.get('bind_port', 6001))
|
||||||
|
|
||||||
|
def run_forever(self):
|
||||||
|
"""
|
||||||
|
Runs container sync scans until stopped.
|
||||||
|
"""
|
||||||
|
sleep(random() * self.interval)
|
||||||
|
while True:
|
||||||
|
begin = time()
|
||||||
|
all_locs = audit_location_generator(self.devices,
|
||||||
|
container_server.DATADIR,
|
||||||
|
mount_check=self.mount_check,
|
||||||
|
logger=self.logger)
|
||||||
|
for path, device, partition in all_locs:
|
||||||
|
self.container_sync(path)
|
||||||
|
if time() - self.reported >= 3600: # once an hour
|
||||||
|
self.report()
|
||||||
|
elapsed = time() - begin
|
||||||
|
if elapsed < self.interval:
|
||||||
|
sleep(self.interval - elapsed)
|
||||||
|
|
||||||
|
def run_once(self):
|
||||||
|
"""
|
||||||
|
Runs a single container sync scan.
|
||||||
|
"""
|
||||||
|
self.logger.info(_('Begin container sync "once" mode'))
|
||||||
|
begin = time()
|
||||||
|
all_locs = audit_location_generator(self.devices,
|
||||||
|
container_server.DATADIR,
|
||||||
|
mount_check=self.mount_check,
|
||||||
|
logger=self.logger)
|
||||||
|
for path, device, partition in all_locs:
|
||||||
|
self.container_sync(path)
|
||||||
|
if time() - self.reported >= 3600: # once an hour
|
||||||
|
self.report()
|
||||||
|
self.report()
|
||||||
|
elapsed = time() - begin
|
||||||
|
self.logger.info(
|
||||||
|
_('Container sync "once" mode completed: %.02fs'), elapsed)
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
"""
|
||||||
|
Writes a report of the stats to the logger and resets the stats for the
|
||||||
|
next report.
|
||||||
|
"""
|
||||||
|
self.logger.info(
|
||||||
|
_('Since %(time)s: %(sync)s synced [%(delete)s deletes, %(put)s '
|
||||||
|
'puts], %(skip)s skipped, %(fail)s failed'),
|
||||||
|
{'time': ctime(self.reported),
|
||||||
|
'sync': self.container_syncs,
|
||||||
|
'delete': self.container_deletes,
|
||||||
|
'put': self.container_puts,
|
||||||
|
'skip': self.container_skips,
|
||||||
|
'fail': self.container_failures})
|
||||||
|
self.reported = time()
|
||||||
|
self.container_syncs = 0
|
||||||
|
self.container_deletes = 0
|
||||||
|
self.container_puts = 0
|
||||||
|
self.container_skips = 0
|
||||||
|
self.container_failures = 0
|
||||||
|
|
||||||
|
def container_sync(self, path):
|
||||||
|
"""
|
||||||
|
Checks the given path for a container database, determines if syncing
|
||||||
|
is turned on for that database and, if so, sends any updates to the
|
||||||
|
other container.
|
||||||
|
|
||||||
|
:param path: the path to a container db
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not path.endswith('.db'):
|
||||||
|
return
|
||||||
|
broker = ContainerBroker(path)
|
||||||
|
info = broker.get_info()
|
||||||
|
x, nodes = self.container_ring.get_nodes(info['account'],
|
||||||
|
info['container'])
|
||||||
|
for ordinal, node in enumerate(nodes):
|
||||||
|
if node['ip'] in self._myips and node['port'] == self._myport:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if not broker.is_deleted():
|
||||||
|
sync_to = None
|
||||||
|
sync_key = None
|
||||||
|
sync_point1 = info['x_container_sync_point1']
|
||||||
|
sync_point2 = info['x_container_sync_point2']
|
||||||
|
for key, (value, timestamp) in broker.metadata.iteritems():
|
||||||
|
if key.lower() == 'x-container-sync-to':
|
||||||
|
sync_to = value
|
||||||
|
elif key.lower() == 'x-container-sync-key':
|
||||||
|
sync_key = value
|
||||||
|
if not sync_to or not sync_key:
|
||||||
|
self.container_skips += 1
|
||||||
|
return
|
||||||
|
sync_to = sync_to.rstrip('/')
|
||||||
|
err = validate_sync_to(sync_to, self.allowed_sync_hosts)
|
||||||
|
if err:
|
||||||
|
self.logger.info(
|
||||||
|
_('ERROR %(db_file)s: %(validate_sync_to_err)s'),
|
||||||
|
{'db_file': broker.db_file,
|
||||||
|
'validate_sync_to_err': err})
|
||||||
|
self.container_failures += 1
|
||||||
|
return
|
||||||
|
stop_at = time() + self.container_time
|
||||||
|
while time() < stop_at and sync_point2 < sync_point1:
|
||||||
|
rows = broker.get_items_since(sync_point2, 1)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
row = rows[0]
|
||||||
|
if row['ROWID'] >= sync_point1:
|
||||||
|
break
|
||||||
|
key = hash_path(info['account'], info['container'],
|
||||||
|
row['name'], raw_digest=True)
|
||||||
|
# This node will only intially sync out one third of the
|
||||||
|
# objects (if 3 replicas, 1/4 if 4, etc.). This section
|
||||||
|
# will attempt to sync previously skipped rows in case the
|
||||||
|
# other nodes didn't succeed.
|
||||||
|
if unpack_from('>I', key)[0] % \
|
||||||
|
self.container_ring.replica_count != ordinal:
|
||||||
|
if not self.container_sync_row(row, sync_to, sync_key,
|
||||||
|
broker, info):
|
||||||
|
return
|
||||||
|
sync_point2 = row['ROWID']
|
||||||
|
broker.set_x_container_sync_points(None, sync_point2)
|
||||||
|
while time() < stop_at:
|
||||||
|
rows = broker.get_items_since(sync_point1, 1)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
row = rows[0]
|
||||||
|
key = hash_path(info['account'], info['container'],
|
||||||
|
row['name'], raw_digest=True)
|
||||||
|
# This node will only intially sync out one third of the
|
||||||
|
# objects (if 3 replicas, 1/4 if 4, etc.). It'll come back
|
||||||
|
# around to the section above and attempt to sync
|
||||||
|
# previously skipped rows in case the other nodes didn't
|
||||||
|
# succeed.
|
||||||
|
if unpack_from('>I', key)[0] % \
|
||||||
|
self.container_ring.replica_count == ordinal:
|
||||||
|
if not self.container_sync_row(row, sync_to, sync_key,
|
||||||
|
broker, info):
|
||||||
|
return
|
||||||
|
sync_point1 = row['ROWID']
|
||||||
|
broker.set_x_container_sync_points(sync_point1, None)
|
||||||
|
self.container_syncs += 1
|
||||||
|
except Exception, err:
|
||||||
|
self.container_failures += 1
|
||||||
|
self.logger.exception(_('ERROR Syncing %s'), (broker.db_file))
|
||||||
|
|
||||||
|
def container_sync_row(self, row, sync_to, sync_key, broker, info):
|
||||||
|
"""
|
||||||
|
Sends the update the row indicates to the sync_to container.
|
||||||
|
|
||||||
|
:param row: The updated row in the local database triggering the sync
|
||||||
|
update.
|
||||||
|
:param sync_to: The URL to the remote container.
|
||||||
|
:param sync_key: The X-Container-Sync-Key to use when sending requests
|
||||||
|
to the other container.
|
||||||
|
:param broker: The local container database broker.
|
||||||
|
:param info: The get_info result from the local container database
|
||||||
|
broker.
|
||||||
|
:returns: True on success
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if row['deleted']:
|
||||||
|
try:
|
||||||
|
delete_object(sync_to, name=row['name'],
|
||||||
|
headers={'x-timestamp': row['created_at'],
|
||||||
|
'x-container-sync-key': sync_key},
|
||||||
|
proxy=self.proxy)
|
||||||
|
except ClientException, err:
|
||||||
|
if err.http_status != 404:
|
||||||
|
raise
|
||||||
|
self.container_deletes += 1
|
||||||
|
else:
|
||||||
|
part, nodes = self.object_ring.get_nodes(
|
||||||
|
info['account'], info['container'],
|
||||||
|
row['name'])
|
||||||
|
shuffle(nodes)
|
||||||
|
exc = None
|
||||||
|
looking_for_timestamp = float(row['created_at'])
|
||||||
|
timestamp = -1
|
||||||
|
headers = body = None
|
||||||
|
for node in nodes:
|
||||||
|
try:
|
||||||
|
these_headers, this_body = direct_get_object(node,
|
||||||
|
part, info['account'], info['container'],
|
||||||
|
row['name'], resp_chunk_size=65536)
|
||||||
|
this_timestamp = float(these_headers['x-timestamp'])
|
||||||
|
if this_timestamp > timestamp:
|
||||||
|
timestamp = this_timestamp
|
||||||
|
headers = these_headers
|
||||||
|
body = this_body
|
||||||
|
except ClientException, err:
|
||||||
|
# If any errors are not 404, make sure we report the
|
||||||
|
# non-404 one. We don't want to mistakenly assume the
|
||||||
|
# object no longer exists just because one says so and
|
||||||
|
# the others errored for some other reason.
|
||||||
|
if not exc or exc.http_status == 404:
|
||||||
|
exc = err
|
||||||
|
if timestamp < looking_for_timestamp:
|
||||||
|
if exc:
|
||||||
|
raise exc
|
||||||
|
raise Exception(_('Unknown exception trying to GET: '
|
||||||
|
'%(node)r %(account)r %(container)r %(object)r'),
|
||||||
|
{'node': node, 'part': part,
|
||||||
|
'account': info['account'],
|
||||||
|
'container': info['container'],
|
||||||
|
'object': row['name']})
|
||||||
|
for key in ('date', 'last-modified'):
|
||||||
|
if key in headers:
|
||||||
|
del headers[key]
|
||||||
|
if 'etag' in headers:
|
||||||
|
headers['etag'] = headers['etag'].strip('"')
|
||||||
|
headers['x-timestamp'] = row['created_at']
|
||||||
|
headers['x-container-sync-key'] = sync_key
|
||||||
|
put_object(sync_to, name=row['name'], headers=headers,
|
||||||
|
contents=_Iter2FileLikeObject(body), proxy=self.proxy)
|
||||||
|
self.container_puts += 1
|
||||||
|
except ClientException, err:
|
||||||
|
if err.http_status == 401:
|
||||||
|
self.logger.info(_('Unauth %(sync_from)r '
|
||||||
|
'=> %(sync_to)r'),
|
||||||
|
{'sync_from': '%s/%s' %
|
||||||
|
(quote(info['account']), quote(info['container'])),
|
||||||
|
'sync_to': sync_to})
|
||||||
|
elif err.http_status == 404:
|
||||||
|
self.logger.info(_('Not found %(sync_from)r '
|
||||||
|
'=> %(sync_to)r'),
|
||||||
|
{'sync_from': '%s/%s' %
|
||||||
|
(quote(info['account']), quote(info['container'])),
|
||||||
|
'sync_to': sync_to})
|
||||||
|
else:
|
||||||
|
self.logger.exception(
|
||||||
|
_('ERROR Syncing %(db_file)s %(row)s'),
|
||||||
|
{'db_file': broker.db_file, 'row': row})
|
||||||
|
self.container_failures += 1
|
||||||
|
return False
|
||||||
|
except Exception, err:
|
||||||
|
self.logger.exception(
|
||||||
|
_('ERROR Syncing %(db_file)s %(row)s'),
|
||||||
|
{'db_file': broker.db_file, 'row': row})
|
||||||
|
self.container_failures += 1
|
||||||
|
return False
|
||||||
|
return True
|
|
@ -500,6 +500,7 @@ class ObjectController(object):
|
||||||
return error_response
|
return error_response
|
||||||
file = DiskFile(self.devices, device, partition, account, container,
|
file = DiskFile(self.devices, device, partition, account, container,
|
||||||
obj, self.logger, disk_chunk_size=self.disk_chunk_size)
|
obj, self.logger, disk_chunk_size=self.disk_chunk_size)
|
||||||
|
orig_timestamp = file.metadata.get('X-Timestamp')
|
||||||
upload_expiration = time.time() + self.max_upload_time
|
upload_expiration = time.time() + self.max_upload_time
|
||||||
etag = md5()
|
etag = md5()
|
||||||
upload_size = 0
|
upload_size = 0
|
||||||
|
@ -544,13 +545,16 @@ class ObjectController(object):
|
||||||
metadata[header_caps] = request.headers[header_key]
|
metadata[header_caps] = request.headers[header_key]
|
||||||
file.put(fd, tmppath, metadata)
|
file.put(fd, tmppath, metadata)
|
||||||
file.unlinkold(metadata['X-Timestamp'])
|
file.unlinkold(metadata['X-Timestamp'])
|
||||||
self.container_update('PUT', account, container, obj, request.headers,
|
if not orig_timestamp or \
|
||||||
{'x-size': file.metadata['Content-Length'],
|
orig_timestamp < request.headers['x-timestamp']:
|
||||||
'x-content-type': file.metadata['Content-Type'],
|
self.container_update('PUT', account, container, obj,
|
||||||
'x-timestamp': file.metadata['X-Timestamp'],
|
request.headers,
|
||||||
'x-etag': file.metadata['ETag'],
|
{'x-size': file.metadata['Content-Length'],
|
||||||
'x-trans-id': request.headers.get('x-trans-id', '-')},
|
'x-content-type': file.metadata['Content-Type'],
|
||||||
device)
|
'x-timestamp': file.metadata['X-Timestamp'],
|
||||||
|
'x-etag': file.metadata['ETag'],
|
||||||
|
'x-trans-id': request.headers.get('x-trans-id', '-')},
|
||||||
|
device)
|
||||||
resp = HTTPCreated(request=request, etag=etag)
|
resp = HTTPCreated(request=request, etag=etag)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -657,6 +661,8 @@ class ObjectController(object):
|
||||||
response.headers[key] = value
|
response.headers[key] = value
|
||||||
response.etag = file.metadata['ETag']
|
response.etag = file.metadata['ETag']
|
||||||
response.last_modified = float(file.metadata['X-Timestamp'])
|
response.last_modified = float(file.metadata['X-Timestamp'])
|
||||||
|
# Needed for container sync feature
|
||||||
|
response.headers['X-Timestamp'] = file.metadata['X-Timestamp']
|
||||||
response.content_length = file_size
|
response.content_length = file_size
|
||||||
if 'Content-Encoding' in file.metadata:
|
if 'Content-Encoding' in file.metadata:
|
||||||
response.content_encoding = file.metadata['Content-Encoding']
|
response.content_encoding = file.metadata['Content-Encoding']
|
||||||
|
@ -680,6 +686,7 @@ class ObjectController(object):
|
||||||
response_class = HTTPNoContent
|
response_class = HTTPNoContent
|
||||||
file = DiskFile(self.devices, device, partition, account, container,
|
file = DiskFile(self.devices, device, partition, account, container,
|
||||||
obj, self.logger, disk_chunk_size=self.disk_chunk_size)
|
obj, self.logger, disk_chunk_size=self.disk_chunk_size)
|
||||||
|
orig_timestamp = file.metadata.get('X-Timestamp')
|
||||||
if file.is_deleted():
|
if file.is_deleted():
|
||||||
response_class = HTTPNotFound
|
response_class = HTTPNotFound
|
||||||
metadata = {
|
metadata = {
|
||||||
|
@ -688,10 +695,12 @@ class ObjectController(object):
|
||||||
with file.mkstemp() as (fd, tmppath):
|
with file.mkstemp() as (fd, tmppath):
|
||||||
file.put(fd, tmppath, metadata, extension='.ts')
|
file.put(fd, tmppath, metadata, extension='.ts')
|
||||||
file.unlinkold(metadata['X-Timestamp'])
|
file.unlinkold(metadata['X-Timestamp'])
|
||||||
self.container_update('DELETE', account, container, obj,
|
if not orig_timestamp or \
|
||||||
request.headers, {'x-timestamp': metadata['X-Timestamp'],
|
orig_timestamp < request.headers['x-timestamp']:
|
||||||
'x-trans-id': request.headers.get('x-trans-id', '-')},
|
self.container_update('DELETE', account, container, obj,
|
||||||
device)
|
request.headers, {'x-timestamp': metadata['X-Timestamp'],
|
||||||
|
'x-trans-id': request.headers.get('x-trans-id', '-')},
|
||||||
|
device)
|
||||||
resp = response_class(request=request)
|
resp = response_class(request=request)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ from webob import Request, Response
|
||||||
|
|
||||||
from swift.common.ring import Ring
|
from swift.common.ring import Ring
|
||||||
from swift.common.utils import cache_from_env, ContextPool, get_logger, \
|
from swift.common.utils import cache_from_env, ContextPool, get_logger, \
|
||||||
normalize_timestamp, split_path, TRUE_VALUES
|
get_remote_client, normalize_timestamp, split_path, TRUE_VALUES
|
||||||
from swift.common.bufferedhttp import http_connect
|
from swift.common.bufferedhttp import http_connect
|
||||||
from swift.common.constraints import check_metadata, check_object_creation, \
|
from swift.common.constraints import check_metadata, check_object_creation, \
|
||||||
check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \
|
check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \
|
||||||
|
@ -418,8 +418,8 @@ class Controller(object):
|
||||||
:param account: account name for the container
|
:param account: account name for the container
|
||||||
:param container: container name to look up
|
:param container: container name to look up
|
||||||
:returns: tuple of (container partition, container nodes, container
|
:returns: tuple of (container partition, container nodes, container
|
||||||
read acl, container write acl) or (None, None, None, None) if
|
read acl, container write acl, container sync key) or (None,
|
||||||
the container does not exist
|
None, None, None, None) if the container does not exist
|
||||||
"""
|
"""
|
||||||
partition, nodes = self.app.container_ring.get_nodes(
|
partition, nodes = self.app.container_ring.get_nodes(
|
||||||
account, container)
|
account, container)
|
||||||
|
@ -431,15 +431,17 @@ class Controller(object):
|
||||||
status = cache_value['status']
|
status = cache_value['status']
|
||||||
read_acl = cache_value['read_acl']
|
read_acl = cache_value['read_acl']
|
||||||
write_acl = cache_value['write_acl']
|
write_acl = cache_value['write_acl']
|
||||||
|
sync_key = cache_value.get('sync_key')
|
||||||
if status == 200:
|
if status == 200:
|
||||||
return partition, nodes, read_acl, write_acl
|
return partition, nodes, read_acl, write_acl, sync_key
|
||||||
elif status == 404:
|
elif status == 404:
|
||||||
return None, None, None, None
|
return None, None, None, None, None
|
||||||
if not self.account_info(account, autocreate=account_autocreate)[1]:
|
if not self.account_info(account, autocreate=account_autocreate)[1]:
|
||||||
return None, None, None, None
|
return None, None, None, None, None
|
||||||
result_code = 0
|
result_code = 0
|
||||||
read_acl = None
|
read_acl = None
|
||||||
write_acl = None
|
write_acl = None
|
||||||
|
sync_key = None
|
||||||
container_size = None
|
container_size = None
|
||||||
attempts_left = self.app.container_ring.replica_count
|
attempts_left = self.app.container_ring.replica_count
|
||||||
headers = {'x-trans-id': self.trans_id}
|
headers = {'x-trans-id': self.trans_id}
|
||||||
|
@ -455,6 +457,7 @@ class Controller(object):
|
||||||
result_code = 200
|
result_code = 200
|
||||||
read_acl = resp.getheader('x-container-read')
|
read_acl = resp.getheader('x-container-read')
|
||||||
write_acl = resp.getheader('x-container-write')
|
write_acl = resp.getheader('x-container-write')
|
||||||
|
sync_key = resp.getheader('x-container-sync-key')
|
||||||
container_size = \
|
container_size = \
|
||||||
resp.getheader('X-Container-Object-Count')
|
resp.getheader('X-Container-Object-Count')
|
||||||
break
|
break
|
||||||
|
@ -483,11 +486,12 @@ class Controller(object):
|
||||||
{'status': result_code,
|
{'status': result_code,
|
||||||
'read_acl': read_acl,
|
'read_acl': read_acl,
|
||||||
'write_acl': write_acl,
|
'write_acl': write_acl,
|
||||||
|
'sync_key': sync_key,
|
||||||
'container_size': container_size},
|
'container_size': container_size},
|
||||||
timeout=cache_timeout)
|
timeout=cache_timeout)
|
||||||
if result_code == 200:
|
if result_code == 200:
|
||||||
return partition, nodes, read_acl, write_acl
|
return partition, nodes, read_acl, write_acl, sync_key
|
||||||
return None, None, None, None
|
return None, None, None, None, None
|
||||||
|
|
||||||
def iter_nodes(self, partition, nodes, ring):
|
def iter_nodes(self, partition, nodes, ring):
|
||||||
"""
|
"""
|
||||||
|
@ -684,6 +688,9 @@ class Controller(object):
|
||||||
raise
|
raise
|
||||||
res.app_iter = file_iter()
|
res.app_iter = file_iter()
|
||||||
update_headers(res, source.getheaders())
|
update_headers(res, source.getheaders())
|
||||||
|
# Used by container sync feature
|
||||||
|
res.environ['swift_x_timestamp'] = \
|
||||||
|
source.getheader('x-timestamp')
|
||||||
update_headers(res, {'accept-ranges': 'bytes'})
|
update_headers(res, {'accept-ranges': 'bytes'})
|
||||||
res.status = source.status
|
res.status = source.status
|
||||||
res.content_length = source.getheader('Content-Length')
|
res.content_length = source.getheader('Content-Length')
|
||||||
|
@ -694,6 +701,9 @@ class Controller(object):
|
||||||
elif 200 <= source.status <= 399:
|
elif 200 <= source.status <= 399:
|
||||||
res = status_map[source.status](request=req)
|
res = status_map[source.status](request=req)
|
||||||
update_headers(res, source.getheaders())
|
update_headers(res, source.getheaders())
|
||||||
|
# Used by container sync feature
|
||||||
|
res.environ['swift_x_timestamp'] = \
|
||||||
|
source.getheader('x-timestamp')
|
||||||
update_headers(res, {'accept-ranges': 'bytes'})
|
update_headers(res, {'accept-ranges': 'bytes'})
|
||||||
if req.method == 'HEAD':
|
if req.method == 'HEAD':
|
||||||
res.content_length = source.getheader('Content-Length')
|
res.content_length = source.getheader('Content-Length')
|
||||||
|
@ -902,7 +912,7 @@ class ObjectController(Controller):
|
||||||
error_response = check_metadata(req, 'object')
|
error_response = check_metadata(req, 'object')
|
||||||
if error_response:
|
if error_response:
|
||||||
return error_response
|
return error_response
|
||||||
container_partition, containers, _junk, req.acl = \
|
container_partition, containers, _junk, req.acl, _junk = \
|
||||||
self.container_info(self.account_name, self.container_name,
|
self.container_info(self.account_name, self.container_name,
|
||||||
account_autocreate=self.app.account_autocreate)
|
account_autocreate=self.app.account_autocreate)
|
||||||
if 'swift.authorize' in req.environ:
|
if 'swift.authorize' in req.environ:
|
||||||
|
@ -960,7 +970,8 @@ class ObjectController(Controller):
|
||||||
@delay_denial
|
@delay_denial
|
||||||
def PUT(self, req):
|
def PUT(self, req):
|
||||||
"""HTTP PUT request handler."""
|
"""HTTP PUT request handler."""
|
||||||
container_partition, containers, _junk, req.acl = \
|
(container_partition, containers, _junk, req.acl,
|
||||||
|
req.environ['swift_sync_key']) = \
|
||||||
self.container_info(self.account_name, self.container_name,
|
self.container_info(self.account_name, self.container_name,
|
||||||
account_autocreate=self.app.account_autocreate)
|
account_autocreate=self.app.account_autocreate)
|
||||||
if 'swift.authorize' in req.environ:
|
if 'swift.authorize' in req.environ:
|
||||||
|
@ -971,7 +982,27 @@ class ObjectController(Controller):
|
||||||
return HTTPNotFound(request=req)
|
return HTTPNotFound(request=req)
|
||||||
partition, nodes = self.app.object_ring.get_nodes(
|
partition, nodes = self.app.object_ring.get_nodes(
|
||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
req.headers['X-Timestamp'] = normalize_timestamp(time.time())
|
# Used by container sync feature
|
||||||
|
if 'x-timestamp' in req.headers:
|
||||||
|
try:
|
||||||
|
req.headers['X-Timestamp'] = \
|
||||||
|
normalize_timestamp(float(req.headers['x-timestamp']))
|
||||||
|
# For container sync PUTs, do a HEAD to see if we can
|
||||||
|
# shortcircuit
|
||||||
|
hreq = Request.blank(req.path_info,
|
||||||
|
environ={'REQUEST_METHOD': 'HEAD'})
|
||||||
|
self.GETorHEAD_base(hreq, _('Object'), partition, nodes,
|
||||||
|
hreq.path_info, self.app.object_ring.replica_count)
|
||||||
|
if 'swift_x_timestamp' in hreq.environ and \
|
||||||
|
float(hreq.environ['swift_x_timestamp']) >= \
|
||||||
|
float(req.headers['x-timestamp']):
|
||||||
|
return HTTPAccepted(request=req)
|
||||||
|
except ValueError:
|
||||||
|
return HTTPBadRequest(request=req, content_type='text/plain',
|
||||||
|
body='X-Timestamp should be a UNIX timestamp float value; '
|
||||||
|
'was %r' % req.headers['x-timestamp'])
|
||||||
|
else:
|
||||||
|
req.headers['X-Timestamp'] = normalize_timestamp(time.time())
|
||||||
# Sometimes the 'content-type' header exists, but is set to None.
|
# Sometimes the 'content-type' header exists, but is set to None.
|
||||||
content_type_manually_set = True
|
content_type_manually_set = True
|
||||||
if not req.headers.get('content-type'):
|
if not req.headers.get('content-type'):
|
||||||
|
@ -1151,7 +1182,8 @@ class ObjectController(Controller):
|
||||||
@delay_denial
|
@delay_denial
|
||||||
def DELETE(self, req):
|
def DELETE(self, req):
|
||||||
"""HTTP DELETE request handler."""
|
"""HTTP DELETE request handler."""
|
||||||
container_partition, containers, _junk, req.acl = \
|
(container_partition, containers, _junk, req.acl,
|
||||||
|
req.environ['swift_sync_key']) = \
|
||||||
self.container_info(self.account_name, self.container_name)
|
self.container_info(self.account_name, self.container_name)
|
||||||
if 'swift.authorize' in req.environ:
|
if 'swift.authorize' in req.environ:
|
||||||
aresp = req.environ['swift.authorize'](req)
|
aresp = req.environ['swift.authorize'](req)
|
||||||
|
@ -1161,7 +1193,17 @@ class ObjectController(Controller):
|
||||||
return HTTPNotFound(request=req)
|
return HTTPNotFound(request=req)
|
||||||
partition, nodes = self.app.object_ring.get_nodes(
|
partition, nodes = self.app.object_ring.get_nodes(
|
||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
req.headers['X-Timestamp'] = normalize_timestamp(time.time())
|
# Used by container sync feature
|
||||||
|
if 'x-timestamp' in req.headers:
|
||||||
|
try:
|
||||||
|
req.headers['X-Timestamp'] = \
|
||||||
|
normalize_timestamp(float(req.headers['x-timestamp']))
|
||||||
|
except ValueError:
|
||||||
|
return HTTPBadRequest(request=req, content_type='text/plain',
|
||||||
|
body='X-Timestamp should be a UNIX timestamp float value; '
|
||||||
|
'was %r' % req.headers['x-timestamp'])
|
||||||
|
else:
|
||||||
|
req.headers['X-Timestamp'] = normalize_timestamp(time.time())
|
||||||
headers = []
|
headers = []
|
||||||
for container in containers:
|
for container in containers:
|
||||||
nheaders = dict(req.headers.iteritems())
|
nheaders = dict(req.headers.iteritems())
|
||||||
|
@ -1207,7 +1249,8 @@ class ContainerController(Controller):
|
||||||
server_type = _('Container')
|
server_type = _('Container')
|
||||||
|
|
||||||
# Ensure these are all lowercase
|
# Ensure these are all lowercase
|
||||||
pass_through_headers = ['x-container-read', 'x-container-write']
|
pass_through_headers = ['x-container-read', 'x-container-write',
|
||||||
|
'x-container-sync-key', 'x-container-sync-to']
|
||||||
|
|
||||||
def __init__(self, app, account_name, container_name, **kwargs):
|
def __init__(self, app, account_name, container_name, **kwargs):
|
||||||
Controller.__init__(self, app)
|
Controller.__init__(self, app)
|
||||||
|
@ -1244,6 +1287,7 @@ class ContainerController(Controller):
|
||||||
{'status': resp.status_int,
|
{'status': resp.status_int,
|
||||||
'read_acl': resp.headers.get('x-container-read'),
|
'read_acl': resp.headers.get('x-container-read'),
|
||||||
'write_acl': resp.headers.get('x-container-write'),
|
'write_acl': resp.headers.get('x-container-write'),
|
||||||
|
'sync_key': resp.headers.get('x-container-sync-key'),
|
||||||
'container_size': resp.headers.get('x-container-object-count')},
|
'container_size': resp.headers.get('x-container-object-count')},
|
||||||
timeout=self.app.recheck_container_existence)
|
timeout=self.app.recheck_container_existence)
|
||||||
|
|
||||||
|
@ -1252,6 +1296,11 @@ class ContainerController(Controller):
|
||||||
aresp = req.environ['swift.authorize'](req)
|
aresp = req.environ['swift.authorize'](req)
|
||||||
if aresp:
|
if aresp:
|
||||||
return aresp
|
return aresp
|
||||||
|
if not req.environ.get('swift_owner', False):
|
||||||
|
for key in ('x-container-read', 'x-container-write',
|
||||||
|
'x-container-sync-key', 'x-container-sync-to'):
|
||||||
|
if key in resp.headers:
|
||||||
|
del resp.headers[key]
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@public
|
@public
|
||||||
|
@ -1646,13 +1695,7 @@ class Application(BaseApplication):
|
||||||
the_request = quote(unquote(req.path))
|
the_request = quote(unquote(req.path))
|
||||||
if req.query_string:
|
if req.query_string:
|
||||||
the_request = the_request + '?' + req.query_string
|
the_request = the_request + '?' + req.query_string
|
||||||
# remote user for zeus
|
client = get_remote_client(req)
|
||||||
client = req.headers.get('x-cluster-client-ip')
|
|
||||||
if not client and 'x-forwarded-for' in req.headers:
|
|
||||||
# remote user for other lbs
|
|
||||||
client = req.headers['x-forwarded-for'].split(',')[0].strip()
|
|
||||||
if not client:
|
|
||||||
client = req.remote_addr
|
|
||||||
logged_headers = None
|
logged_headers = None
|
||||||
if self.log_headers:
|
if self.log_headers:
|
||||||
logged_headers = '\n'.join('%s: %s' % (k, v)
|
logged_headers = '\n'.join('%s: %s' % (k, v)
|
||||||
|
|
|
@ -61,8 +61,8 @@ def reset_environment():
|
||||||
print 'Giving up after %s retries.' % attempt
|
print 'Giving up after %s retries.' % attempt
|
||||||
raise err
|
raise err
|
||||||
print err
|
print err
|
||||||
print 'Retrying in 1 second...'
|
print 'Retrying in 2 seconds...'
|
||||||
sleep(1)
|
sleep(2)
|
||||||
except BaseException, err:
|
except BaseException, err:
|
||||||
kill_pids(pids)
|
kill_pids(pids)
|
||||||
raise err
|
raise err
|
||||||
|
|
|
@ -139,13 +139,16 @@ def mock_time():
|
||||||
|
|
||||||
class TestRateLimit(unittest.TestCase):
|
class TestRateLimit(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def _reset_time(self):
|
||||||
global time_ticker
|
global time_ticker
|
||||||
time_ticker = 0
|
time_ticker = 0
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
self.was_sleep = eventlet.sleep
|
self.was_sleep = eventlet.sleep
|
||||||
eventlet.sleep = mock_sleep
|
eventlet.sleep = mock_sleep
|
||||||
self.was_time = time.time
|
self.was_time = time.time
|
||||||
time.time = mock_time
|
time.time = mock_time
|
||||||
|
self._reset_time()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
eventlet.sleep = self.was_sleep
|
eventlet.sleep = self.was_sleep
|
||||||
|
@ -186,31 +189,34 @@ class TestRateLimit(unittest.TestCase):
|
||||||
logger=FakeLogger())
|
logger=FakeLogger())
|
||||||
the_app.memcache_client = fake_memcache
|
the_app.memcache_client = fake_memcache
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||||
'GET', 'a', None, None)), 1)
|
'DELETE', 'a', None, None)), 0)
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
|
||||||
'POST', 'a', 'c', None)), 0)
|
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||||
'PUT', 'a', 'c', None)), 1)
|
'PUT', 'a', 'c', None)), 1)
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||||
'GET', 'a', 'c', None)), 1)
|
'DELETE', 'a', 'c', None)), 1)
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||||
'GET', 'a', 'c', 'o')), 0)
|
'GET', 'a', 'c', 'o')), 0)
|
||||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||||
'PUT', 'a', 'c', 'o')), 1)
|
'PUT', 'a', 'c', 'o')), 1)
|
||||||
|
|
||||||
def test_ratelimit(self):
|
def test_account_ratelimit(self):
|
||||||
current_rate = 5
|
current_rate = 5
|
||||||
num_calls = 50
|
num_calls = 50
|
||||||
conf_dict = {'account_ratelimit': current_rate}
|
conf_dict = {'account_ratelimit': current_rate}
|
||||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||||
ratelimit.http_connect = mock_http_connect(204)
|
ratelimit.http_connect = mock_http_connect(204)
|
||||||
req = Request.blank('/v/a')
|
for meth, exp_time in [('DELETE', 9.8), ('GET', 0),
|
||||||
req.environ['swift.cache'] = FakeMemcache()
|
('POST', 0), ('PUT', 9.8)]:
|
||||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
req = Request.blank('/v/a%s/c' % meth)
|
||||||
start_response)
|
req.method = meth
|
||||||
begin = time.time()
|
req.environ['swift.cache'] = FakeMemcache()
|
||||||
self._run(make_app_call, num_calls, current_rate)
|
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||||
self.assertEquals(round(time.time() - begin, 1), 9.8)
|
start_response)
|
||||||
|
begin = time.time()
|
||||||
|
self._run(make_app_call, num_calls, current_rate,
|
||||||
|
check_time=bool(exp_time))
|
||||||
|
self.assertEquals(round(time.time() - begin, 1), exp_time)
|
||||||
|
self._reset_time()
|
||||||
|
|
||||||
def test_ratelimit_set_incr(self):
|
def test_ratelimit_set_incr(self):
|
||||||
current_rate = 5
|
current_rate = 5
|
||||||
|
@ -218,7 +224,8 @@ class TestRateLimit(unittest.TestCase):
|
||||||
conf_dict = {'account_ratelimit': current_rate}
|
conf_dict = {'account_ratelimit': current_rate}
|
||||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||||
ratelimit.http_connect = mock_http_connect(204)
|
ratelimit.http_connect = mock_http_connect(204)
|
||||||
req = Request.blank('/v/a')
|
req = Request.blank('/v/a/c')
|
||||||
|
req.method = 'PUT'
|
||||||
req.environ['swift.cache'] = FakeMemcache()
|
req.environ['swift.cache'] = FakeMemcache()
|
||||||
req.environ['swift.cache'].init_incr_return_neg = True
|
req.environ['swift.cache'].init_incr_return_neg = True
|
||||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||||
|
@ -306,7 +313,8 @@ class TestRateLimit(unittest.TestCase):
|
||||||
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
||||||
ratelimit.http_connect = mock_http_connect(204)
|
ratelimit.http_connect = mock_http_connect(204)
|
||||||
self.test_ratelimit.log_sleep_time_seconds = .00001
|
self.test_ratelimit.log_sleep_time_seconds = .00001
|
||||||
req = Request.blank('/v/a')
|
req = Request.blank('/v/a/c')
|
||||||
|
req.method = 'PUT'
|
||||||
req.environ['swift.cache'] = FakeMemcache()
|
req.environ['swift.cache'] = FakeMemcache()
|
||||||
|
|
||||||
time_override = [0, 0, 0, 0, None]
|
time_override = [0, 0, 0, 0, None]
|
||||||
|
@ -335,7 +343,7 @@ class TestRateLimit(unittest.TestCase):
|
||||||
logger=FakeLogger())
|
logger=FakeLogger())
|
||||||
the_app.memcache_client = fake_memcache
|
the_app.memcache_client = fake_memcache
|
||||||
req = lambda: None
|
req = lambda: None
|
||||||
req.method = 'GET'
|
req.method = 'PUT'
|
||||||
|
|
||||||
class rate_caller(Thread):
|
class rate_caller(Thread):
|
||||||
|
|
||||||
|
@ -346,7 +354,7 @@ class TestRateLimit(unittest.TestCase):
|
||||||
def run(self):
|
def run(self):
|
||||||
for j in range(num_calls):
|
for j in range(num_calls):
|
||||||
self.result = the_app.handle_ratelimit(req, self.myname,
|
self.result = the_app.handle_ratelimit(req, self.myname,
|
||||||
None, None)
|
'c', None)
|
||||||
|
|
||||||
nt = 15
|
nt = 15
|
||||||
begin = time.time()
|
begin = time.time()
|
||||||
|
@ -361,45 +369,6 @@ class TestRateLimit(unittest.TestCase):
|
||||||
time_took = time.time() - begin
|
time_took = time.time() - begin
|
||||||
self.assertEquals(1.5, round(time_took, 1))
|
self.assertEquals(1.5, round(time_took, 1))
|
||||||
|
|
||||||
def test_ratelimit_acc_vrs_container(self):
|
|
||||||
conf_dict = {'clock_accuracy': 1000,
|
|
||||||
'account_ratelimit': 10,
|
|
||||||
'max_sleep_time_seconds': 4,
|
|
||||||
'container_ratelimit_10': 6,
|
|
||||||
'container_ratelimit_50': 2,
|
|
||||||
'container_ratelimit_75': 1}
|
|
||||||
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
|
||||||
ratelimit.http_connect = mock_http_connect(204)
|
|
||||||
req = Request.blank('/v/a/c')
|
|
||||||
req.environ['swift.cache'] = FakeMemcache()
|
|
||||||
cont_key = get_container_memcache_key('a', 'c')
|
|
||||||
|
|
||||||
class rate_caller(Thread):
|
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.parent = parent
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.result = self.parent.test_ratelimit(req.environ,
|
|
||||||
start_response)
|
|
||||||
|
|
||||||
def runthreads(threads, nt):
|
|
||||||
for i in range(nt):
|
|
||||||
rc = rate_caller(self, "thread %s" % i)
|
|
||||||
rc.start()
|
|
||||||
threads.append(rc)
|
|
||||||
for thread in threads:
|
|
||||||
thread.join()
|
|
||||||
begin = time.time()
|
|
||||||
req.environ['swift.cache'].set(cont_key, {'container_size': 20})
|
|
||||||
begin = time.time()
|
|
||||||
threads = []
|
|
||||||
runthreads(threads, 3)
|
|
||||||
time_took = time.time() - begin
|
|
||||||
self.assertEquals(round(time_took, 1), .4)
|
|
||||||
|
|
||||||
def test_call_invalid_path(self):
|
def test_call_invalid_path(self):
|
||||||
env = {'REQUEST_METHOD': 'GET',
|
env = {'REQUEST_METHOD': 'GET',
|
||||||
'SCRIPT_NAME': '',
|
'SCRIPT_NAME': '',
|
||||||
|
@ -441,7 +410,8 @@ class TestRateLimit(unittest.TestCase):
|
||||||
conf_dict = {'account_ratelimit': current_rate}
|
conf_dict = {'account_ratelimit': current_rate}
|
||||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||||
ratelimit.http_connect = mock_http_connect(204)
|
ratelimit.http_connect = mock_http_connect(204)
|
||||||
req = Request.blank('/v/a')
|
req = Request.blank('/v/a/c')
|
||||||
|
req.method = 'PUT'
|
||||||
req.environ['swift.cache'] = FakeMemcache()
|
req.environ['swift.cache'] = FakeMemcache()
|
||||||
req.environ['swift.cache'].error_on_incr = True
|
req.environ['swift.cache'].error_on_incr = True
|
||||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||||
|
|
|
@ -573,6 +573,15 @@ class TestSwift3(unittest.TestCase):
|
||||||
verify('7506d97002c7d2de922cc0ec34af8846', '/bucket/object',
|
verify('7506d97002c7d2de922cc0ec34af8846', '/bucket/object',
|
||||||
{'Content-Type': None, 'X-Amz-Something': 'test'})
|
{'Content-Type': None, 'X-Amz-Something': 'test'})
|
||||||
|
|
||||||
|
verify('28f76d6162444a193b612cd6cb20e0be', '/bucket/object',
|
||||||
|
{'Content-Type': None,
|
||||||
|
'X-Amz-Date': 'Mon, 11 Jul 2011 10:52:57 +0000',
|
||||||
|
'Date': 'Tue, 12 Jul 2011 10:52:57 +0000'})
|
||||||
|
|
||||||
|
verify('ed6971e3eca5af4ee361f05d7c272e49', '/bucket/object',
|
||||||
|
{'Content-Type': None,
|
||||||
|
'Date': 'Tue, 12 Jul 2011 10:52:57 +0000'})
|
||||||
|
|
||||||
req1 = Request.blank('/', headers=
|
req1 = Request.blank('/', headers=
|
||||||
{'Content-Type': None, 'X-Amz-Something': 'test'})
|
{'Content-Type': None, 'X-Amz-Something': 'test'})
|
||||||
req2 = Request.blank('/', headers=
|
req2 = Request.blank('/', headers=
|
||||||
|
|
|
@ -56,15 +56,21 @@ class FakeMemcache(object):
|
||||||
|
|
||||||
class FakeApp(object):
|
class FakeApp(object):
|
||||||
|
|
||||||
def __init__(self, status_headers_body_iter=None):
|
def __init__(self, status_headers_body_iter=None, acl=None, sync_key=None):
|
||||||
self.calls = 0
|
self.calls = 0
|
||||||
self.status_headers_body_iter = status_headers_body_iter
|
self.status_headers_body_iter = status_headers_body_iter
|
||||||
if not self.status_headers_body_iter:
|
if not self.status_headers_body_iter:
|
||||||
self.status_headers_body_iter = iter([('404 Not Found', {}, '')])
|
self.status_headers_body_iter = iter([('404 Not Found', {}, '')])
|
||||||
|
self.acl = acl
|
||||||
|
self.sync_key = sync_key
|
||||||
|
|
||||||
def __call__(self, env, start_response):
|
def __call__(self, env, start_response):
|
||||||
self.calls += 1
|
self.calls += 1
|
||||||
self.request = Request.blank('', environ=env)
|
self.request = Request.blank('', environ=env)
|
||||||
|
if self.acl:
|
||||||
|
self.request.acl = self.acl
|
||||||
|
if self.sync_key:
|
||||||
|
self.request.environ['swift_sync_key'] = self.sync_key
|
||||||
if 'swift.authorize' in env:
|
if 'swift.authorize' in env:
|
||||||
resp = env['swift.authorize'](self.request)
|
resp = env['swift.authorize'](self.request)
|
||||||
if resp:
|
if resp:
|
||||||
|
@ -138,7 +144,8 @@ class TestAuth(unittest.TestCase):
|
||||||
self.assertEquals(resp.status_int, 404)
|
self.assertEquals(resp.status_int, 404)
|
||||||
|
|
||||||
def test_anon(self):
|
def test_anon(self):
|
||||||
resp = self._make_request('/v1/AUTH_account').get_response(self.test_auth)
|
resp = \
|
||||||
|
self._make_request('/v1/AUTH_account').get_response(self.test_auth)
|
||||||
self.assertEquals(resp.status_int, 401)
|
self.assertEquals(resp.status_int, 401)
|
||||||
self.assertEquals(resp.environ['swift.authorize'],
|
self.assertEquals(resp.environ['swift.authorize'],
|
||||||
self.test_auth.authorize)
|
self.test_auth.authorize)
|
||||||
|
@ -289,30 +296,35 @@ class TestAuth(unittest.TestCase):
|
||||||
self.assertEquals(self.test_auth.authorize(req), None)
|
self.assertEquals(self.test_auth.authorize(req), None)
|
||||||
|
|
||||||
def test_account_put_permissions(self):
|
def test_account_put_permissions(self):
|
||||||
req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
|
req = self._make_request('/v1/AUTH_new',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'})
|
||||||
req.remote_user = 'act:usr,act'
|
req.remote_user = 'act:usr,act'
|
||||||
resp = self.test_auth.authorize(req)
|
resp = self.test_auth.authorize(req)
|
||||||
self.assertEquals(resp.status_int, 403)
|
self.assertEquals(resp.status_int, 403)
|
||||||
|
|
||||||
req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
|
req = self._make_request('/v1/AUTH_new',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'})
|
||||||
req.remote_user = 'act:usr,act,AUTH_other'
|
req.remote_user = 'act:usr,act,AUTH_other'
|
||||||
resp = self.test_auth.authorize(req)
|
resp = self.test_auth.authorize(req)
|
||||||
self.assertEquals(resp.status_int, 403)
|
self.assertEquals(resp.status_int, 403)
|
||||||
|
|
||||||
# Even PUTs to your own account as account admin should fail
|
# Even PUTs to your own account as account admin should fail
|
||||||
req = self._make_request('/v1/AUTH_old', environ={'REQUEST_METHOD': 'PUT'})
|
req = self._make_request('/v1/AUTH_old',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'})
|
||||||
req.remote_user = 'act:usr,act,AUTH_old'
|
req.remote_user = 'act:usr,act,AUTH_old'
|
||||||
resp = self.test_auth.authorize(req)
|
resp = self.test_auth.authorize(req)
|
||||||
self.assertEquals(resp.status_int, 403)
|
self.assertEquals(resp.status_int, 403)
|
||||||
|
|
||||||
req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
|
req = self._make_request('/v1/AUTH_new',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'})
|
||||||
req.remote_user = 'act:usr,act,.reseller_admin'
|
req.remote_user = 'act:usr,act,.reseller_admin'
|
||||||
resp = self.test_auth.authorize(req)
|
resp = self.test_auth.authorize(req)
|
||||||
self.assertEquals(resp, None)
|
self.assertEquals(resp, None)
|
||||||
|
|
||||||
# .super_admin is not something the middleware should ever see or care
|
# .super_admin is not something the middleware should ever see or care
|
||||||
# about
|
# about
|
||||||
req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
|
req = self._make_request('/v1/AUTH_new',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'})
|
||||||
req.remote_user = 'act:usr,act,.super_admin'
|
req.remote_user = 'act:usr,act,.super_admin'
|
||||||
resp = self.test_auth.authorize(req)
|
resp = self.test_auth.authorize(req)
|
||||||
self.assertEquals(resp.status_int, 403)
|
self.assertEquals(resp.status_int, 403)
|
||||||
|
@ -383,6 +395,152 @@ class TestAuth(unittest.TestCase):
|
||||||
headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth)
|
headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth)
|
||||||
self.assertEquals(resp.status_int, 401)
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
def test_allowed_sync_hosts(self):
|
||||||
|
a = auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp())
|
||||||
|
self.assertEquals(a.allowed_sync_hosts, ['127.0.0.1'])
|
||||||
|
a = auth.filter_factory({'super_admin_key': 'supertest',
|
||||||
|
'allowed_sync_hosts':
|
||||||
|
'1.1.1.1,2.1.1.1, 3.1.1.1 , 4.1.1.1,, , 5.1.1.1'})(FakeApp())
|
||||||
|
self.assertEquals(a.allowed_sync_hosts,
|
||||||
|
['1.1.1.1', '2.1.1.1', '3.1.1.1', '4.1.1.1', '5.1.1.1'])
|
||||||
|
|
||||||
|
def test_reseller_admin_is_owner(self):
|
||||||
|
orig_authorize = self.test_auth.authorize
|
||||||
|
owner_values = []
|
||||||
|
|
||||||
|
def mitm_authorize(req):
|
||||||
|
rv = orig_authorize(req)
|
||||||
|
owner_values.append(req.environ.get('swift_owner', False))
|
||||||
|
return rv
|
||||||
|
|
||||||
|
self.test_auth.authorize = mitm_authorize
|
||||||
|
|
||||||
|
req = self._make_request('/v1/AUTH_cfa',
|
||||||
|
headers={'X-Auth-Token': 'AUTH_t'})
|
||||||
|
req.remote_user = '.reseller_admin'
|
||||||
|
self.test_auth.authorize(req)
|
||||||
|
self.assertEquals(owner_values, [True])
|
||||||
|
|
||||||
|
def test_admin_is_owner(self):
|
||||||
|
orig_authorize = self.test_auth.authorize
|
||||||
|
owner_values = []
|
||||||
|
|
||||||
|
def mitm_authorize(req):
|
||||||
|
rv = orig_authorize(req)
|
||||||
|
owner_values.append(req.environ.get('swift_owner', False))
|
||||||
|
return rv
|
||||||
|
|
||||||
|
self.test_auth.authorize = mitm_authorize
|
||||||
|
|
||||||
|
req = self._make_request('/v1/AUTH_cfa',
|
||||||
|
headers={'X-Auth-Token': 'AUTH_t'})
|
||||||
|
req.remote_user = 'AUTH_cfa'
|
||||||
|
self.test_auth.authorize(req)
|
||||||
|
self.assertEquals(owner_values, [True])
|
||||||
|
|
||||||
|
def test_regular_is_not_owner(self):
|
||||||
|
orig_authorize = self.test_auth.authorize
|
||||||
|
owner_values = []
|
||||||
|
|
||||||
|
def mitm_authorize(req):
|
||||||
|
rv = orig_authorize(req)
|
||||||
|
owner_values.append(req.environ.get('swift_owner', False))
|
||||||
|
return rv
|
||||||
|
|
||||||
|
self.test_auth.authorize = mitm_authorize
|
||||||
|
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c',
|
||||||
|
headers={'X-Auth-Token': 'AUTH_t'})
|
||||||
|
req.remote_user = 'act:usr'
|
||||||
|
self.test_auth.authorize(req)
|
||||||
|
self.assertEquals(owner_values, [False])
|
||||||
|
|
||||||
|
def test_sync_request_success(self):
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456'})
|
||||||
|
req.remote_addr = '127.0.0.1'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 204)
|
||||||
|
|
||||||
|
def test_sync_request_fail_key(self):
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'wrongsecret',
|
||||||
|
'x-timestamp': '123.456'})
|
||||||
|
req.remote_addr = '127.0.0.1'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='othersecret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456'})
|
||||||
|
req.remote_addr = '127.0.0.1'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key=None)
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456'})
|
||||||
|
req.remote_addr = '127.0.0.1'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
def test_sync_request_fail_no_timestamp(self):
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret'})
|
||||||
|
req.remote_addr = '127.0.0.1'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
def test_sync_request_fail_sync_host(self):
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456'})
|
||||||
|
req.remote_addr = '127.0.0.2'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 401)
|
||||||
|
|
||||||
|
def test_sync_request_success_lb_sync_host(self):
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456',
|
||||||
|
'x-forwarded-for': '127.0.0.1'})
|
||||||
|
req.remote_addr = '127.0.0.2'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 204)
|
||||||
|
|
||||||
|
self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]),
|
||||||
|
sync_key='secret')
|
||||||
|
req = self._make_request('/v1/AUTH_cfa/c/o',
|
||||||
|
environ={'REQUEST_METHOD': 'DELETE'},
|
||||||
|
headers={'x-container-sync-key': 'secret',
|
||||||
|
'x-timestamp': '123.456',
|
||||||
|
'x-cluster-client-ip': '127.0.0.1'})
|
||||||
|
req.remote_addr = '127.0.0.2'
|
||||||
|
resp = req.get_response(self.test_auth)
|
||||||
|
self.assertEquals(resp.status_int, 204)
|
||||||
|
|
||||||
|
|
||||||
class TestParseUserCreation(unittest.TestCase):
|
class TestParseUserCreation(unittest.TestCase):
|
||||||
def test_parse_user_creation(self):
|
def test_parse_user_creation(self):
|
||||||
|
|
|
@ -155,8 +155,8 @@ class MockHttpTest(unittest.TestCase):
|
||||||
def fake_http_connection(*args, **kwargs):
|
def fake_http_connection(*args, **kwargs):
|
||||||
_orig_http_connection = c.http_connection
|
_orig_http_connection = c.http_connection
|
||||||
|
|
||||||
def wrapper(url):
|
def wrapper(url, proxy=None):
|
||||||
parsed, _conn = _orig_http_connection(url)
|
parsed, _conn = _orig_http_connection(url, proxy=proxy)
|
||||||
conn = fake_http_connect(*args, **kwargs)()
|
conn = fake_http_connect(*args, **kwargs)()
|
||||||
|
|
||||||
def request(*args, **kwargs):
|
def request(*args, **kwargs):
|
||||||
|
@ -430,7 +430,7 @@ class TestConnection(MockHttpTest):
|
||||||
def read(self, *args, **kwargs):
|
def read(self, *args, **kwargs):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def local_http_connection(url):
|
def local_http_connection(url, proxy=None):
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
return parsed, LocalConnection()
|
return parsed, LocalConnection()
|
||||||
|
|
||||||
|
|
|
@ -893,6 +893,23 @@ class TestContainerBroker(unittest.TestCase):
|
||||||
self.assertEquals(info['object_count'], 0)
|
self.assertEquals(info['object_count'], 0)
|
||||||
self.assertEquals(info['bytes_used'], 0)
|
self.assertEquals(info['bytes_used'], 0)
|
||||||
|
|
||||||
|
info = broker.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
|
||||||
|
def test_set_x_syncs(self):
|
||||||
|
broker = ContainerBroker(':memory:', account='test1', container='test2')
|
||||||
|
broker.initialize(normalize_timestamp('1'))
|
||||||
|
|
||||||
|
info = broker.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
|
||||||
|
broker.set_x_container_sync_points(1, 2)
|
||||||
|
info = broker.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], 1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], 2)
|
||||||
|
|
||||||
def test_get_report_info(self):
|
def test_get_report_info(self):
|
||||||
broker = ContainerBroker(':memory:', account='test1', container='test2')
|
broker = ContainerBroker(':memory:', account='test1', container='test2')
|
||||||
broker.initialize(normalize_timestamp('1'))
|
broker.initialize(normalize_timestamp('1'))
|
||||||
|
@ -1352,6 +1369,81 @@ class TestContainerBrokerBeforeMetadata(TestContainerBroker):
|
||||||
conn.execute('SELECT metadata FROM container_stat')
|
conn.execute('SELECT metadata FROM container_stat')
|
||||||
|
|
||||||
|
|
||||||
|
def prexsync_create_container_stat_table(self, conn, put_timestamp=None):
|
||||||
|
"""
|
||||||
|
Copied from swift.common.db.ContainerBroker before the
|
||||||
|
x_container_sync_point[12] columns were added; used for testing with
|
||||||
|
TestContainerBrokerBeforeXSync.
|
||||||
|
|
||||||
|
Create the container_stat table which is specifc to the container DB.
|
||||||
|
|
||||||
|
:param conn: DB connection object
|
||||||
|
:param put_timestamp: put timestamp
|
||||||
|
"""
|
||||||
|
if put_timestamp is None:
|
||||||
|
put_timestamp = normalize_timestamp(0)
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE container_stat (
|
||||||
|
account TEXT,
|
||||||
|
container TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
put_timestamp TEXT DEFAULT '0',
|
||||||
|
delete_timestamp TEXT DEFAULT '0',
|
||||||
|
object_count INTEGER,
|
||||||
|
bytes_used INTEGER,
|
||||||
|
reported_put_timestamp TEXT DEFAULT '0',
|
||||||
|
reported_delete_timestamp TEXT DEFAULT '0',
|
||||||
|
reported_object_count INTEGER DEFAULT 0,
|
||||||
|
reported_bytes_used INTEGER DEFAULT 0,
|
||||||
|
hash TEXT default '00000000000000000000000000000000',
|
||||||
|
id TEXT,
|
||||||
|
status TEXT DEFAULT '',
|
||||||
|
status_changed_at TEXT DEFAULT '0',
|
||||||
|
metadata TEXT DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO container_stat (object_count, bytes_used)
|
||||||
|
VALUES (0, 0);
|
||||||
|
""")
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE container_stat
|
||||||
|
SET account = ?, container = ?, created_at = ?, id = ?,
|
||||||
|
put_timestamp = ?
|
||||||
|
''', (self.account, self.container, normalize_timestamp(time()),
|
||||||
|
str(uuid4()), put_timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
class TestContainerBrokerBeforeXSync(TestContainerBroker):
|
||||||
|
"""
|
||||||
|
Tests for swift.common.db.ContainerBroker against databases created before
|
||||||
|
the x_container_sync_point[12] columns were added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._imported_create_container_stat_table = \
|
||||||
|
ContainerBroker.create_container_stat_table
|
||||||
|
ContainerBroker.create_container_stat_table = \
|
||||||
|
prexsync_create_container_stat_table
|
||||||
|
broker = ContainerBroker(':memory:', account='a', container='c')
|
||||||
|
broker.initialize(normalize_timestamp('1'))
|
||||||
|
exc = None
|
||||||
|
with broker.get() as conn:
|
||||||
|
try:
|
||||||
|
conn.execute('''SELECT x_container_sync_point1
|
||||||
|
FROM container_stat''')
|
||||||
|
except BaseException, err:
|
||||||
|
exc = err
|
||||||
|
self.assert_('no such column: x_container_sync_point1' in str(exc))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
ContainerBroker.create_container_stat_table = \
|
||||||
|
self._imported_create_container_stat_table
|
||||||
|
broker = ContainerBroker(':memory:', account='a', container='c')
|
||||||
|
broker.initialize(normalize_timestamp('1'))
|
||||||
|
with broker.get() as conn:
|
||||||
|
conn.execute('SELECT x_container_sync_point1 FROM container_stat')
|
||||||
|
|
||||||
|
|
||||||
class TestAccountBroker(unittest.TestCase):
|
class TestAccountBroker(unittest.TestCase):
|
||||||
""" Tests for swift.common.db.AccountBroker """
|
""" Tests for swift.common.db.AccountBroker """
|
||||||
|
|
||||||
|
|
|
@ -768,6 +768,26 @@ log_name = yarr'''
|
||||||
self.assertEquals(utils.human_readable(1237940039285380274899124224),
|
self.assertEquals(utils.human_readable(1237940039285380274899124224),
|
||||||
'1024Yi')
|
'1024Yi')
|
||||||
|
|
||||||
|
def test_validate_sync_to(self):
|
||||||
|
for goodurl in ('http://1.1.1.1/v1/a/c/o',
|
||||||
|
'http://1.1.1.1:8080/a/c/o',
|
||||||
|
'http://2.2.2.2/a/c/o',
|
||||||
|
'https://1.1.1.1/v1/a/c/o'):
|
||||||
|
self.assertEquals(utils.validate_sync_to(goodurl,
|
||||||
|
['1.1.1.1', '2.2.2.2']),
|
||||||
|
None)
|
||||||
|
for badurl in ('http://1.1.1.1',
|
||||||
|
'httpq://1.1.1.1/v1/a/c/o',
|
||||||
|
'http://1.1.1.1/v1/a/c/o?query',
|
||||||
|
'http://1.1.1.1/v1/a/c/o#frag',
|
||||||
|
'http://1.1.1.1/v1/a/c/o?query#frag',
|
||||||
|
'http://1.1.1.1/v1/a/c/o?query=param',
|
||||||
|
'http://1.1.1.1/v1/a/c/o?query=param#frag',
|
||||||
|
'http://1.1.1.2/v1/a/c/o'):
|
||||||
|
self.assertNotEquals(utils.validate_sync_to(badurl,
|
||||||
|
['1.1.1.1', '2.2.2.2']),
|
||||||
|
None)
|
||||||
|
|
||||||
def test_TRUE_VALUES(self):
|
def test_TRUE_VALUES(self):
|
||||||
for v in utils.TRUE_VALUES:
|
for v in utils.TRUE_VALUES:
|
||||||
self.assertEquals(v, v.lower())
|
self.assertEquals(v, v.lower())
|
||||||
|
|
|
@ -324,6 +324,76 @@ class TestContainerController(unittest.TestCase):
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
self.assert_(not got_exc)
|
self.assert_(not got_exc)
|
||||||
|
|
||||||
|
def test_PUT_reset_container_sync(self):
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c'})
|
||||||
|
resp = self.controller.PUT(req)
|
||||||
|
self.assertEquals(resp.status_int, 201)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
db.set_x_container_sync_points(123, 456)
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], 123)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], 456)
|
||||||
|
# Set to same value
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c'})
|
||||||
|
resp = self.controller.PUT(req)
|
||||||
|
self.assertEquals(resp.status_int, 202)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], 123)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], 456)
|
||||||
|
# Set to new value
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c2'})
|
||||||
|
resp = self.controller.PUT(req)
|
||||||
|
self.assertEquals(resp.status_int, 202)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
|
||||||
|
def test_POST_reset_container_sync(self):
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c'})
|
||||||
|
resp = self.controller.PUT(req)
|
||||||
|
self.assertEquals(resp.status_int, 201)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
db.set_x_container_sync_points(123, 456)
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], 123)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], 456)
|
||||||
|
# Set to same value
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'POST'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c'})
|
||||||
|
resp = self.controller.POST(req)
|
||||||
|
self.assertEquals(resp.status_int, 204)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], 123)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], 456)
|
||||||
|
# Set to new value
|
||||||
|
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'POST'},
|
||||||
|
headers={'x-timestamp': '1',
|
||||||
|
'x-container-sync-to': 'http://127.0.0.1:12345/v1/a/c2'})
|
||||||
|
resp = self.controller.POST(req)
|
||||||
|
self.assertEquals(resp.status_int, 204)
|
||||||
|
db = self.controller._get_container_broker('sda1', 'p', 'a', 'c')
|
||||||
|
info = db.get_info()
|
||||||
|
self.assertEquals(info['x_container_sync_point1'], -1)
|
||||||
|
self.assertEquals(info['x_container_sync_point2'], -1)
|
||||||
|
|
||||||
def test_DELETE(self):
|
def test_DELETE(self):
|
||||||
req = Request.blank('/sda1/p/a/c',
|
req = Request.blank('/sda1/p/a/c',
|
||||||
environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': '1'})
|
environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': '1'})
|
||||||
|
|
|
@ -0,0 +1,824 @@
|
||||||
|
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from swift.container import sync
|
||||||
|
from swift.common import utils
|
||||||
|
from swift.common.client import ClientException
|
||||||
|
|
||||||
|
|
||||||
|
utils.HASH_PATH_SUFFIX = 'endcap'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRing(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.replica_count = 3
|
||||||
|
self.devs = [{'ip': '10.0.0.%s' % x, 'port': 1000 + x, 'device': 'sda'}
|
||||||
|
for x in xrange(3)]
|
||||||
|
|
||||||
|
def get_nodes(self, account, container=None, obj=None):
|
||||||
|
return 1, list(self.devs)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeContainerBroker(object):
|
||||||
|
|
||||||
|
def __init__(self, path, metadata=None, info=None, deleted=False,
|
||||||
|
items_since=None):
|
||||||
|
self.db_file = path
|
||||||
|
self.metadata = metadata if metadata else {}
|
||||||
|
self.info = info if info else {}
|
||||||
|
self.deleted = deleted
|
||||||
|
self.items_since = items_since if items_since else []
|
||||||
|
self.sync_point1 = -1
|
||||||
|
self.sync_point2 = -1
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
return self.info
|
||||||
|
|
||||||
|
def is_deleted(self):
|
||||||
|
return self.deleted
|
||||||
|
|
||||||
|
def get_items_since(self, sync_point, limit):
|
||||||
|
if sync_point < 0:
|
||||||
|
sync_point = 0
|
||||||
|
return self.items_since[sync_point:sync_point + limit]
|
||||||
|
|
||||||
|
def set_x_container_sync_points(self, sync_point1, sync_point2):
|
||||||
|
self.sync_point1 = sync_point1
|
||||||
|
self.sync_point2 = sync_point2
|
||||||
|
|
||||||
|
|
||||||
|
class TestContainerSync(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_Iter2FileLikeObject(self):
|
||||||
|
flo = sync._Iter2FileLikeObject(iter(['123', '4567', '89', '0']))
|
||||||
|
expect = '1234567890'
|
||||||
|
|
||||||
|
got = flo.read(2)
|
||||||
|
self.assertTrue(len(got) <= 2)
|
||||||
|
self.assertEquals(got, expect[:len(got)])
|
||||||
|
expect = expect[len(got):]
|
||||||
|
|
||||||
|
got = flo.read(5)
|
||||||
|
self.assertTrue(len(got) <= 5)
|
||||||
|
self.assertEquals(got, expect[:len(got)])
|
||||||
|
expect = expect[len(got):]
|
||||||
|
|
||||||
|
self.assertEquals(flo.read(), expect)
|
||||||
|
self.assertEquals(flo.read(), '')
|
||||||
|
self.assertEquals(flo.read(2), '')
|
||||||
|
|
||||||
|
flo = sync._Iter2FileLikeObject(iter(['123', '4567', '89', '0']))
|
||||||
|
self.assertEquals(flo.read(), '1234567890')
|
||||||
|
self.assertEquals(flo.read(), '')
|
||||||
|
self.assertEquals(flo.read(2), '')
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
self.assertTrue(cs.container_ring is cring)
|
||||||
|
self.assertTrue(cs.object_ring is oring)
|
||||||
|
|
||||||
|
def test_run_forever(self):
|
||||||
|
# This runs runs_forever with fakes to succeed for two loops, the first
|
||||||
|
# causing a report but no interval sleep, the second no report but an
|
||||||
|
# interval sleep.
|
||||||
|
time_calls = [0]
|
||||||
|
sleep_calls = []
|
||||||
|
audit_location_generator_calls = [0]
|
||||||
|
|
||||||
|
def fake_time():
|
||||||
|
time_calls[0] += 1
|
||||||
|
returns = [1, # Initialized reported time
|
||||||
|
1, # Start time
|
||||||
|
3602, # Is it report time (yes)
|
||||||
|
3602, # Report time
|
||||||
|
3602, # Elapsed time for "under interval" (no)
|
||||||
|
3602, # Start time
|
||||||
|
3603, # Is it report time (no)
|
||||||
|
3603, # Elapsed time for "under interval" (yes)
|
||||||
|
]
|
||||||
|
if time_calls[0] == len(returns) + 1:
|
||||||
|
raise Exception('we are now done')
|
||||||
|
return returns[time_calls[0] - 1]
|
||||||
|
|
||||||
|
def fake_sleep(amount):
|
||||||
|
sleep_calls.append(amount)
|
||||||
|
|
||||||
|
def fake_audit_location_generator(*args, **kwargs):
|
||||||
|
audit_location_generator_calls[0] += 1
|
||||||
|
# Makes .container_sync() short-circuit because 'path' doesn't end
|
||||||
|
# with .db
|
||||||
|
return [('path', 'device', 'partition')]
|
||||||
|
|
||||||
|
orig_time = sync.time
|
||||||
|
orig_sleep = sync.sleep
|
||||||
|
orig_audit_location_generator = sync.audit_location_generator
|
||||||
|
try:
|
||||||
|
sync.time = fake_time
|
||||||
|
sync.sleep = fake_sleep
|
||||||
|
sync.audit_location_generator = fake_audit_location_generator
|
||||||
|
cs = sync.ContainerSync({}, container_ring=FakeRing(),
|
||||||
|
object_ring=FakeRing())
|
||||||
|
cs.run_forever()
|
||||||
|
except Exception, err:
|
||||||
|
if str(err) != 'we are now done':
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
sync.time = orig_time
|
||||||
|
sync.sleep = orig_sleep
|
||||||
|
sync.audit_location_generator = orig_audit_location_generator
|
||||||
|
|
||||||
|
self.assertEquals(time_calls, [9])
|
||||||
|
self.assertEquals(len(sleep_calls), 2)
|
||||||
|
self.assertTrue(sleep_calls[0] <= cs.interval)
|
||||||
|
self.assertTrue(sleep_calls[1] == cs.interval - 1)
|
||||||
|
self.assertEquals(audit_location_generator_calls, [2])
|
||||||
|
self.assertEquals(cs.reported, 3602)
|
||||||
|
|
||||||
|
def test_run_once(self):
|
||||||
|
# This runs runs_once with fakes twice, the first causing an interim
|
||||||
|
# report, the second with no interm report.
|
||||||
|
time_calls = [0]
|
||||||
|
audit_location_generator_calls = [0]
|
||||||
|
|
||||||
|
def fake_time():
|
||||||
|
time_calls[0] += 1
|
||||||
|
returns = [1, # Initialized reported time
|
||||||
|
1, # Start time
|
||||||
|
3602, # Is it report time (yes)
|
||||||
|
3602, # Report time
|
||||||
|
3602, # End report time
|
||||||
|
3602, # For elapsed
|
||||||
|
3602, # Start time
|
||||||
|
3603, # Is it report time (no)
|
||||||
|
3604, # End report time
|
||||||
|
3605, # For elapsed
|
||||||
|
]
|
||||||
|
if time_calls[0] == len(returns) + 1:
|
||||||
|
raise Exception('we are now done')
|
||||||
|
return returns[time_calls[0] - 1]
|
||||||
|
|
||||||
|
def fake_audit_location_generator(*args, **kwargs):
|
||||||
|
audit_location_generator_calls[0] += 1
|
||||||
|
# Makes .container_sync() short-circuit because 'path' doesn't end
|
||||||
|
# with .db
|
||||||
|
return [('path', 'device', 'partition')]
|
||||||
|
|
||||||
|
orig_time = sync.time
|
||||||
|
orig_audit_location_generator = sync.audit_location_generator
|
||||||
|
try:
|
||||||
|
sync.time = fake_time
|
||||||
|
sync.audit_location_generator = fake_audit_location_generator
|
||||||
|
cs = sync.ContainerSync({}, container_ring=FakeRing(),
|
||||||
|
object_ring=FakeRing())
|
||||||
|
cs.run_once()
|
||||||
|
self.assertEquals(time_calls, [6])
|
||||||
|
self.assertEquals(audit_location_generator_calls, [1])
|
||||||
|
self.assertEquals(cs.reported, 3602)
|
||||||
|
cs.run_once()
|
||||||
|
except Exception, err:
|
||||||
|
if str(err) != 'we are now done':
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
sync.time = orig_time
|
||||||
|
sync.audit_location_generator = orig_audit_location_generator
|
||||||
|
|
||||||
|
self.assertEquals(time_calls, [10])
|
||||||
|
self.assertEquals(audit_location_generator_calls, [2])
|
||||||
|
self.assertEquals(cs.reported, 3604)
|
||||||
|
|
||||||
|
def test_container_sync_not_db(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
|
||||||
|
def test_container_sync_missing_db(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
|
||||||
|
def test_container_sync_not_my_db(self):
|
||||||
|
# Db could be there due to handoff replication so test that we ignore
|
||||||
|
# those.
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
try:
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c'})
|
||||||
|
cs._myips = ['127.0.0.1'] # No match
|
||||||
|
cs._myport = 1 # No match
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1 # No match
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
|
||||||
|
cs._myips = ['127.0.0.1'] # No match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
# This complete match will cause the 1 container failure since the
|
||||||
|
# broker's info doesn't contain sync point keys
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
|
||||||
|
def test_container_sync_deleted(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
try:
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c'}, deleted=False)
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
# This complete match will cause the 1 container failure since the
|
||||||
|
# broker's info doesn't contain sync point keys
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c'}, deleted=True)
|
||||||
|
# This complete match will not cause any more container failures
|
||||||
|
# since the broker indicates deletion
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
|
||||||
|
def test_container_sync_no_to_or_key(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
try:
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1})
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
# This complete match will be skipped since the broker's metadata
|
||||||
|
# has no x-container-sync-to or x-container-sync-key
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 1)
|
||||||
|
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1)})
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
# This complete match will be skipped since the broker's metadata
|
||||||
|
# has no x-container-sync-key
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 2)
|
||||||
|
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-key': ('key', 1)})
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
# This complete match will be skipped since the broker's metadata
|
||||||
|
# has no x-container-sync-to
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 3)
|
||||||
|
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)})
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = []
|
||||||
|
# This complete match will cause a container failure since the
|
||||||
|
# sync-to won't validate as allowed.
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 3)
|
||||||
|
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)})
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
# This complete match will succeed completely since the broker
|
||||||
|
# get_items_since will return no new rows.
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 3)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
|
||||||
|
def test_container_stop_at(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
orig_time = sync.time
|
||||||
|
try:
|
||||||
|
sync.ContainerBroker = lambda p: FakeContainerBroker(p,
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=['erroneous data'])
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
# This sync will fail since the items_since data is bad.
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
|
||||||
|
# Set up fake times to make the sync short-circuit as having taken
|
||||||
|
# too long
|
||||||
|
fake_times = [
|
||||||
|
1.0, # Compute the time to move on
|
||||||
|
100000.0, # Compute if it's time to move on from first loop
|
||||||
|
100000.0] # Compute if it's time to move on from second loop
|
||||||
|
|
||||||
|
def fake_time():
|
||||||
|
return fake_times.pop(0)
|
||||||
|
|
||||||
|
sync.time = fake_time
|
||||||
|
# This same sync won't fail since it will look like it took so long
|
||||||
|
# as to be time to move on (before it ever actually tries to do
|
||||||
|
# anything).
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
sync.time = orig_time
|
||||||
|
|
||||||
|
def test_container_first_loop(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
orig_hash_path = sync.hash_path
|
||||||
|
orig_delete_object = sync.delete_object
|
||||||
|
try:
|
||||||
|
|
||||||
|
def fake_hash_path(account, container, obj, raw_digest=False):
|
||||||
|
# Ensures that no rows match for full syncing, ordinal is 0 and
|
||||||
|
# all hashes are 0
|
||||||
|
return '\x00' * 16
|
||||||
|
|
||||||
|
sync.hash_path = fake_hash_path
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': 2,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o'}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Succeeds because no rows match
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, None)
|
||||||
|
self.assertEquals(fcb.sync_point2, 1)
|
||||||
|
|
||||||
|
def fake_hash_path(account, container, obj, raw_digest=False):
|
||||||
|
# Ensures that all rows match for full syncing, ordinal is 0
|
||||||
|
# and all hashes are 1
|
||||||
|
return '\x01' * 16
|
||||||
|
|
||||||
|
sync.hash_path = fake_hash_path
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': 1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o'}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Succeeds because the two sync points haven't deviated enough yet
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, -1)
|
||||||
|
self.assertEquals(fcb.sync_point2, -1)
|
||||||
|
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': 2,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o'}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Fails because container_sync_row will fail since the row has no
|
||||||
|
# 'deleted' key
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, -1)
|
||||||
|
self.assertEquals(fcb.sync_point2, -1)
|
||||||
|
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': 2,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o', 'created_at': '1.2',
|
||||||
|
'deleted': True}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Fails because delete_object fails
|
||||||
|
self.assertEquals(cs.container_failures, 2)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, -1)
|
||||||
|
self.assertEquals(fcb.sync_point2, -1)
|
||||||
|
|
||||||
|
def fake_delete_object(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': 2,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o', 'created_at': '1.2',
|
||||||
|
'deleted': True}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Succeeds because delete_object succeeds
|
||||||
|
self.assertEquals(cs.container_failures, 2)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, None)
|
||||||
|
self.assertEquals(fcb.sync_point2, 1)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
sync.hash_path = orig_hash_path
|
||||||
|
sync.delete_object = orig_delete_object
|
||||||
|
|
||||||
|
def test_container_second_loop(self):
|
||||||
|
cring = FakeRing()
|
||||||
|
oring = FakeRing()
|
||||||
|
cs = sync.ContainerSync({}, container_ring=cring, object_ring=oring)
|
||||||
|
orig_ContainerBroker = sync.ContainerBroker
|
||||||
|
orig_hash_path = sync.hash_path
|
||||||
|
orig_delete_object = sync.delete_object
|
||||||
|
try:
|
||||||
|
# We'll ensure the first loop is always skipped by keeping the two
|
||||||
|
# sync points equal
|
||||||
|
|
||||||
|
def fake_hash_path(account, container, obj, raw_digest=False):
|
||||||
|
# Ensures that no rows match for second loop, ordinal is 0 and
|
||||||
|
# all hashes are 1
|
||||||
|
return '\x01' * 16
|
||||||
|
|
||||||
|
sync.hash_path = fake_hash_path
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o'}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Succeeds because no rows match
|
||||||
|
self.assertEquals(cs.container_failures, 0)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, 1)
|
||||||
|
self.assertEquals(fcb.sync_point2, None)
|
||||||
|
|
||||||
|
def fake_hash_path(account, container, obj, raw_digest=False):
|
||||||
|
# Ensures that all rows match for second loop, ordinal is 0 and
|
||||||
|
# all hashes are 0
|
||||||
|
return '\x00' * 16
|
||||||
|
|
||||||
|
def fake_delete_object(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
sync.hash_path = fake_hash_path
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o'}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Fails because row is missing 'deleted' key
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, -1)
|
||||||
|
self.assertEquals(fcb.sync_point2, -1)
|
||||||
|
|
||||||
|
fcb = FakeContainerBroker('path',
|
||||||
|
info={'account': 'a', 'container': 'c',
|
||||||
|
'x_container_sync_point1': -1,
|
||||||
|
'x_container_sync_point2': -1},
|
||||||
|
metadata={'x-container-sync-to': ('http://127.0.0.1/a/c', 1),
|
||||||
|
'x-container-sync-key': ('key', 1)},
|
||||||
|
items_since=[{'ROWID': 1, 'name': 'o', 'created_at': '1.2',
|
||||||
|
'deleted': True}])
|
||||||
|
sync.ContainerBroker = lambda p: fcb
|
||||||
|
cs._myips = ['10.0.0.0'] # Match
|
||||||
|
cs._myport = 1000 # Match
|
||||||
|
cs.allowed_sync_hosts = ['127.0.0.1']
|
||||||
|
cs.container_sync('isa.db')
|
||||||
|
# Succeeds because row now has 'deleted' key and delete_object
|
||||||
|
# succeeds
|
||||||
|
self.assertEquals(cs.container_failures, 1)
|
||||||
|
self.assertEquals(cs.container_skips, 0)
|
||||||
|
self.assertEquals(fcb.sync_point1, 1)
|
||||||
|
self.assertEquals(fcb.sync_point2, None)
|
||||||
|
finally:
|
||||||
|
sync.ContainerBroker = orig_ContainerBroker
|
||||||
|
sync.hash_path = orig_hash_path
|
||||||
|
sync.delete_object = orig_delete_object
|
||||||
|
|
||||||
|
def test_container_sync_row_delete(self):
|
||||||
|
orig_delete_object = sync.delete_object
|
||||||
|
try:
|
||||||
|
|
||||||
|
def fake_delete_object(path, name=None, headers=None, proxy=None):
|
||||||
|
self.assertEquals(path, 'http://sync/to/path')
|
||||||
|
self.assertEquals(name, 'object')
|
||||||
|
self.assertEquals(headers,
|
||||||
|
{'x-container-sync-key': 'key', 'x-timestamp': '1.2'})
|
||||||
|
self.assertEquals(proxy, 'http://proxy')
|
||||||
|
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
cs = sync.ContainerSync({}, container_ring=FakeRing(),
|
||||||
|
object_ring=FakeRing())
|
||||||
|
cs.proxy = 'http://proxy'
|
||||||
|
# Success
|
||||||
|
self.assertTrue(cs.container_sync_row({'deleted': True,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), 'info'))
|
||||||
|
self.assertEquals(cs.container_deletes, 1)
|
||||||
|
|
||||||
|
exc = []
|
||||||
|
|
||||||
|
def fake_delete_object(path, name=None, headers=None, proxy=None):
|
||||||
|
exc.append(Exception('test exception'))
|
||||||
|
raise exc[-1]
|
||||||
|
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
# Failure because of delete_object exception
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': True,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), 'info'))
|
||||||
|
self.assertEquals(cs.container_deletes, 1)
|
||||||
|
self.assertEquals(len(exc), 1)
|
||||||
|
self.assertEquals(str(exc[-1]), 'test exception')
|
||||||
|
|
||||||
|
def fake_delete_object(path, name=None, headers=None, proxy=None):
|
||||||
|
exc.append(ClientException('test client exception'))
|
||||||
|
raise exc[-1]
|
||||||
|
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
# Failure because of delete_object exception
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': True,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), 'info'))
|
||||||
|
self.assertEquals(cs.container_deletes, 1)
|
||||||
|
self.assertEquals(len(exc), 2)
|
||||||
|
self.assertEquals(str(exc[-1]), 'test client exception')
|
||||||
|
|
||||||
|
def fake_delete_object(path, name=None, headers=None, proxy=None):
|
||||||
|
exc.append(ClientException('test client exception',
|
||||||
|
http_status=404))
|
||||||
|
raise exc[-1]
|
||||||
|
|
||||||
|
sync.delete_object = fake_delete_object
|
||||||
|
# Success because the object wasn't even found
|
||||||
|
self.assertTrue(cs.container_sync_row({'deleted': True,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), 'info'))
|
||||||
|
self.assertEquals(cs.container_deletes, 2)
|
||||||
|
self.assertEquals(len(exc), 3)
|
||||||
|
self.assertEquals(str(exc[-1]), 'test client exception: 404')
|
||||||
|
finally:
|
||||||
|
sync.delete_object = orig_delete_object
|
||||||
|
|
||||||
|
def test_container_sync_row_put(self):
|
||||||
|
orig_shuffle = sync.shuffle
|
||||||
|
orig_put_object = sync.put_object
|
||||||
|
orig_direct_get_object = sync.direct_get_object
|
||||||
|
try:
|
||||||
|
sync.shuffle = lambda x: x
|
||||||
|
|
||||||
|
def fake_put_object(sync_to, name=None, headers=None,
|
||||||
|
contents=None, proxy=None):
|
||||||
|
self.assertEquals(sync_to, 'http://sync/to/path')
|
||||||
|
self.assertEquals(name, 'object')
|
||||||
|
self.assertEquals(headers, {'x-container-sync-key': 'key',
|
||||||
|
'x-timestamp': '1.2',
|
||||||
|
'other-header': 'other header value',
|
||||||
|
'etag': 'etagvalue'})
|
||||||
|
self.assertEquals(contents.read(), 'contents')
|
||||||
|
self.assertEquals(proxy, 'http://proxy')
|
||||||
|
|
||||||
|
sync.put_object = fake_put_object
|
||||||
|
|
||||||
|
cs = sync.ContainerSync({}, container_ring=FakeRing(),
|
||||||
|
object_ring=FakeRing())
|
||||||
|
cs.proxy = 'http://proxy'
|
||||||
|
|
||||||
|
def fake_direct_get_object(node, part, account, container, obj,
|
||||||
|
resp_chunk_size=1):
|
||||||
|
return ({'other-header': 'other header value',
|
||||||
|
'etag': '"etagvalue"', 'x-timestamp': '1.2'},
|
||||||
|
iter('contents'))
|
||||||
|
|
||||||
|
sync.direct_get_object = fake_direct_get_object
|
||||||
|
# Success as everything says it worked
|
||||||
|
self.assertTrue(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 1)
|
||||||
|
|
||||||
|
def fake_direct_get_object(node, part, account, container, obj,
|
||||||
|
resp_chunk_size=1):
|
||||||
|
return ({'date': 'date value',
|
||||||
|
'last-modified': 'last modified value',
|
||||||
|
'x-timestamp': '1.2',
|
||||||
|
'other-header': 'other header value',
|
||||||
|
'etag': '"etagvalue"'},
|
||||||
|
iter('contents'))
|
||||||
|
|
||||||
|
sync.direct_get_object = fake_direct_get_object
|
||||||
|
# Success as everything says it worked, also checks 'date' and
|
||||||
|
# 'last-modified' headers are removed and that 'etag' header is
|
||||||
|
# stripped of double quotes.
|
||||||
|
self.assertTrue(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
|
||||||
|
exc = []
|
||||||
|
|
||||||
|
def fake_direct_get_object(node, part, account, container, obj,
|
||||||
|
resp_chunk_size=1):
|
||||||
|
exc.append(Exception('test exception'))
|
||||||
|
raise exc[-1]
|
||||||
|
|
||||||
|
sync.direct_get_object = fake_direct_get_object
|
||||||
|
# Fail due to completely unexpected exception
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
self.assertEquals(len(exc), 1)
|
||||||
|
self.assertEquals(str(exc[-1]), 'test exception')
|
||||||
|
|
||||||
|
def fake_direct_get_object(node, part, account, container, obj,
|
||||||
|
resp_chunk_size=1):
|
||||||
|
exc.append(ClientException('test client exception'))
|
||||||
|
raise exc[-1]
|
||||||
|
|
||||||
|
sync.direct_get_object = fake_direct_get_object
|
||||||
|
# Fail due to all direct_get_object calls failing
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
self.assertEquals(len(exc), 4)
|
||||||
|
self.assertEquals(str(exc[-1]), 'test client exception')
|
||||||
|
|
||||||
|
def fake_direct_get_object(node, part, account, container, obj,
|
||||||
|
resp_chunk_size=1):
|
||||||
|
return ({'other-header': 'other header value',
|
||||||
|
'x-timestamp': '1.2', 'etag': '"etagvalue"'},
|
||||||
|
iter('contents'))
|
||||||
|
|
||||||
|
def fake_put_object(sync_to, name=None, headers=None,
|
||||||
|
contents=None, proxy=None):
|
||||||
|
raise ClientException('test client exception', http_status=401)
|
||||||
|
|
||||||
|
class FakeLogger(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.err = ''
|
||||||
|
self.exc = ''
|
||||||
|
|
||||||
|
def info(self, err, *args, **kwargs):
|
||||||
|
self.err = err
|
||||||
|
|
||||||
|
def exception(self, exc, *args, **kwargs):
|
||||||
|
self.exc = exc
|
||||||
|
|
||||||
|
sync.direct_get_object = fake_direct_get_object
|
||||||
|
sync.put_object = fake_put_object
|
||||||
|
cs.logger = FakeLogger()
|
||||||
|
# Fail due to 401
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
self.assertTrue(cs.logger.err.startswith('Unauth '))
|
||||||
|
|
||||||
|
def fake_put_object(sync_to, name=None, headers=None,
|
||||||
|
contents=None, proxy=None):
|
||||||
|
raise ClientException('test client exception', http_status=404)
|
||||||
|
|
||||||
|
sync.put_object = fake_put_object
|
||||||
|
# Fail due to 404
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
self.assertTrue(cs.logger.err.startswith('Not found '))
|
||||||
|
|
||||||
|
def fake_put_object(sync_to, name=None, headers=None,
|
||||||
|
contents=None, proxy=None):
|
||||||
|
raise ClientException('test client exception', http_status=503)
|
||||||
|
|
||||||
|
sync.put_object = fake_put_object
|
||||||
|
# Fail due to 503
|
||||||
|
self.assertFalse(cs.container_sync_row({'deleted': False,
|
||||||
|
'name': 'object', 'created_at': '1.2'}, 'http://sync/to/path',
|
||||||
|
'key', FakeContainerBroker('broker'), {'account': 'a',
|
||||||
|
'container': 'c'}))
|
||||||
|
self.assertEquals(cs.container_puts, 2)
|
||||||
|
self.assertTrue(cs.logger.exc.startswith('ERROR Syncing '))
|
||||||
|
finally:
|
||||||
|
sync.shuffle = orig_shuffle
|
||||||
|
sync.put_object = orig_put_object
|
||||||
|
sync.direct_get_object = orig_direct_get_object
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue