
Client already supports -H/--header option when creating container or uploading objects. This patch extends this option to support Storage Policy. e.g., swift post con -H 'X-Storage-Policy:p1' This creates one container 'con' with storage policy 'p1'. swift upload con obj -H 'X-Storage-Policy:p2' This creates container 'con' with storage policy 'p2' and uploads object 'obj' into it. Also fixes segmented uploading to non-default storage policy container When uploading large objects with segmentation to container with non-default storage policy, there will be another 'xxx_segments' container created, but with the default storage policy. This results all the segments to be stored with the wrong policy. This patch is for the Storage Policy feature, and also compatible with old versions w/o Storage Policy support. Change-Id: I5c19e90604a0bcf2c85e1732b8a0b97ae6801994
348 lines
14 KiB
Python
348 lines
14 KiB
Python
# Copyright (c) 2014 Christian Schwede <christian.schwede@enovance.com>
|
|
#
|
|
# 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 mock
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
|
|
import six
|
|
|
|
import swiftclient
|
|
import swiftclient.shell
|
|
|
|
|
|
if six.PY2:
|
|
BUILTIN_OPEN = '__builtin__.open'
|
|
else:
|
|
BUILTIN_OPEN = 'builtins.open'
|
|
|
|
mocked_os_environ = {
|
|
'ST_AUTH': 'http://localhost:8080/auth/v1.0',
|
|
'ST_USER': 'test:tester',
|
|
'ST_KEY': 'testing'
|
|
}
|
|
|
|
|
|
@mock.patch.dict(os.environ, mocked_os_environ)
|
|
class TestShell(unittest.TestCase):
|
|
def __init__(self, *args, **kwargs):
|
|
super(TestShell, self).__init__(*args, **kwargs)
|
|
tmpfile = tempfile.NamedTemporaryFile(delete=False)
|
|
self.tmpfile = tmpfile.name
|
|
|
|
def tearDown(self):
|
|
try:
|
|
os.remove(self.tmpfile)
|
|
except OSError:
|
|
pass
|
|
|
|
@mock.patch('swiftclient.shell.MultiThreadingManager._print')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_stat_account(self, connection, mock_print):
|
|
argv = ["", "stat"]
|
|
return_headers = {
|
|
'x-account-container-count': '1',
|
|
'x-account-object-count': '2',
|
|
'x-account-bytes-used': '3',
|
|
'content-length': 0,
|
|
'date': ''}
|
|
connection.return_value.head_account.return_value = return_headers
|
|
connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account'
|
|
swiftclient.shell.main(argv)
|
|
calls = [mock.call(' Account: AUTH_account\n' +
|
|
' Containers: 1\n' +
|
|
' Objects: 2\n' +
|
|
' Bytes: 3'),
|
|
mock.call('')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
@mock.patch('swiftclient.shell.MultiThreadingManager._print')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_stat_container(self, connection, mock_print):
|
|
return_headers = {
|
|
'x-container-object-count': '1',
|
|
'x-container-bytes-used': '2',
|
|
'x-container-read': 'test2:tester2',
|
|
'x-container-write': 'test3:tester3',
|
|
'x-container-sync-to': 'other',
|
|
'x-container-sync-key': 'secret',
|
|
}
|
|
argv = ["", "stat", "container"]
|
|
connection.return_value.head_container.return_value = return_headers
|
|
connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account'
|
|
swiftclient.shell.main(argv)
|
|
calls = [mock.call(' Account: AUTH_account\n' +
|
|
' Container: container\n' +
|
|
' Objects: 1\n' +
|
|
' Bytes: 2\n' +
|
|
' Read ACL: test2:tester2\n' +
|
|
' Write ACL: test3:tester3\n' +
|
|
' Sync To: other\n' +
|
|
' Sync Key: secret'),
|
|
mock.call('')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
@mock.patch('swiftclient.shell.MultiThreadingManager._print')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_stat_object(self, connection, mock_print):
|
|
return_headers = {
|
|
'x-object-manifest': 'manifest',
|
|
'etag': 'md5',
|
|
'last-modified': 'yesterday',
|
|
'content-type': 'text/plain',
|
|
'content-length': 42,
|
|
}
|
|
argv = ["", "stat", "container", "object"]
|
|
connection.return_value.head_object.return_value = return_headers
|
|
connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account'
|
|
swiftclient.shell.main(argv)
|
|
calls = [mock.call(' Account: AUTH_account\n' +
|
|
' Container: container\n' +
|
|
' Object: object\n' +
|
|
' Content Type: text/plain\n' +
|
|
'Content Length: 42\n' +
|
|
' Last Modified: yesterday\n' +
|
|
' ETag: md5\n' +
|
|
' Manifest: manifest'),
|
|
mock.call('')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
@mock.patch('swiftclient.shell.MultiThreadingManager._print')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_list_account(self, connection, mock_print):
|
|
# Test account listing
|
|
connection.return_value.get_account.side_effect = [
|
|
[None, [{'name': 'container'}]],
|
|
[None, []],
|
|
]
|
|
|
|
argv = ["", "list"]
|
|
swiftclient.shell.main(argv)
|
|
calls = [mock.call(marker='', prefix=None),
|
|
mock.call(marker='container', prefix=None)]
|
|
connection.return_value.get_account.assert_has_calls(calls)
|
|
calls = [mock.call('container')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
@mock.patch('swiftclient.shell.MultiThreadingManager._print')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_list_container(self, connection, mock_print):
|
|
connection.return_value.get_container.side_effect = [
|
|
[None, [{'name': 'object_a'}]],
|
|
[None, []],
|
|
]
|
|
argv = ["", "list", "container"]
|
|
swiftclient.shell.main(argv)
|
|
calls = [
|
|
mock.call('container', marker='', delimiter=None, prefix=None),
|
|
mock.call('container', marker='object_a',
|
|
delimiter=None, prefix=None)]
|
|
connection.return_value.get_container.assert_has_calls(calls)
|
|
calls = [mock.call('object_a')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
# Test container listing with --long
|
|
connection.return_value.get_container.side_effect = [
|
|
[None, [{'name': 'object_a', 'bytes': 0,
|
|
'last_modified': '123T456'}]],
|
|
[None, []],
|
|
]
|
|
argv = ["", "list", "container", "--long"]
|
|
swiftclient.shell.main(argv)
|
|
calls = [
|
|
mock.call('container', marker='', delimiter=None, prefix=None),
|
|
mock.call('container', marker='object_a',
|
|
delimiter=None, prefix=None)]
|
|
connection.return_value.get_container.assert_has_calls(calls)
|
|
calls = [mock.call('object_a'),
|
|
mock.call(' 0 123 456 object_a'),
|
|
mock.call(' 0')]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_download(self, connection):
|
|
connection.return_value.get_object.return_value = [
|
|
{'content-type': 'text/plain',
|
|
'etag': 'd41d8cd98f00b204e9800998ecf8427e'},
|
|
'']
|
|
|
|
# Test downloading whole container
|
|
connection.return_value.get_container.side_effect = [
|
|
[None, [{'name': 'object'}]],
|
|
[None, []],
|
|
]
|
|
connection.return_value.auth_end_time = 0
|
|
connection.return_value.attempts = 0
|
|
|
|
with mock.patch(BUILTIN_OPEN) as mock_open:
|
|
argv = ["", "download", "container"]
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.get_object.assert_called_with(
|
|
'container', 'object', headers={}, resp_chunk_size=65536)
|
|
mock_open.assert_called_with('object', 'wb')
|
|
|
|
# Test downloading single object
|
|
with mock.patch(BUILTIN_OPEN) as mock_open:
|
|
argv = ["", "download", "container", "object"]
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.get_object.assert_called_with(
|
|
'container', 'object', headers={}, resp_chunk_size=65536)
|
|
mock_open.assert_called_with('object', 'wb')
|
|
|
|
@mock.patch('swiftclient.shell.listdir')
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_upload(self, connection, listdir):
|
|
connection.return_value.head_object.return_value = {
|
|
'content-length': '0'}
|
|
connection.return_value.attempts = 0
|
|
argv = ["", "upload", "container", self.tmpfile,
|
|
"-H", "X-Storage-Policy:one"]
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.put_container.assert_called_with(
|
|
'container',
|
|
{'X-Storage-Policy': mock.ANY})
|
|
|
|
connection.return_value.put_object.assert_called_with(
|
|
'container',
|
|
self.tmpfile.lstrip('/'),
|
|
mock.ANY,
|
|
content_length=0,
|
|
headers={'x-object-meta-mtime': mock.ANY,
|
|
'X-Storage-Policy': 'one'})
|
|
|
|
# Upload whole directory
|
|
argv = ["", "upload", "container", "/tmp"]
|
|
listdir.return_value = [self.tmpfile]
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.put_object.assert_called_with(
|
|
'container',
|
|
self.tmpfile.lstrip('/'),
|
|
mock.ANY,
|
|
content_length=0,
|
|
headers={'x-object-meta-mtime': mock.ANY})
|
|
|
|
# Upload in segments
|
|
connection.return_value.head_container.return_value = {
|
|
'x-storage-policy': 'one'}
|
|
argv = ["", "upload", "container", self.tmpfile, "-S", "10"]
|
|
with open(self.tmpfile, "wb") as fh:
|
|
fh.write(b'12345678901234567890')
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.put_container.assert_called_with(
|
|
'container_segments',
|
|
{'X-Storage-Policy': mock.ANY})
|
|
connection.return_value.put_object.assert_called_with(
|
|
'container',
|
|
self.tmpfile.lstrip('/'),
|
|
'',
|
|
content_length=0,
|
|
headers={'x-object-manifest': mock.ANY,
|
|
'x-object-meta-mtime': mock.ANY})
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_delete_account(self, connection):
|
|
connection.return_value.get_account.side_effect = [
|
|
[None, [{'name': 'container'}]],
|
|
[None, []],
|
|
]
|
|
connection.return_value.get_container.side_effect = [
|
|
[None, [{'name': 'object'}]],
|
|
[None, []],
|
|
]
|
|
connection.return_value.attempts = 0
|
|
argv = ["", "delete", "--all"]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.delete_container.assert_called_with(
|
|
'container')
|
|
connection.return_value.delete_object.assert_called_with(
|
|
'container', 'object', query_string=None)
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_delete_container(self, connection):
|
|
connection.return_value.get_container.side_effect = [
|
|
[None, [{'name': 'object'}]],
|
|
[None, []],
|
|
]
|
|
connection.return_value.attempts = 0
|
|
argv = ["", "delete", "container"]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.delete_container.assert_called_with(
|
|
'container')
|
|
connection.return_value.delete_object.assert_called_with(
|
|
'container', 'object', query_string=None)
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_delete_object(self, connection):
|
|
argv = ["", "delete", "container", "object"]
|
|
connection.return_value.head_object.return_value = {}
|
|
connection.return_value.attempts = 0
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.delete_object.assert_called_with(
|
|
'container', 'object', query_string=None)
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_post_account(self, connection):
|
|
argv = ["", "post"]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.post_account.assert_called_with(
|
|
headers={})
|
|
|
|
argv = ["", "post", "container"]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.post_container.assert_called_with(
|
|
'container', headers={})
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_post_container(self, connection):
|
|
argv = ["", "post", "container",
|
|
"--read-acl", "test2:tester2",
|
|
"--write-acl", "test3:tester3 test4",
|
|
"--sync-to", "othersite",
|
|
"--sync-key", "secret",
|
|
]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.post_container.assert_called_with(
|
|
'container', headers={
|
|
'X-Container-Write': 'test3:tester3 test4',
|
|
'X-Container-Read': 'test2:tester2',
|
|
'X-Container-Sync-Key': 'secret',
|
|
'X-Container-Sync-To': 'othersite'})
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_post_object(self, connection):
|
|
argv = ["", "post", "container", "object",
|
|
"--meta", "Color:Blue",
|
|
"--header", "content-type:text/plain"
|
|
]
|
|
connection.return_value.head_object.return_value = {}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.post_object.assert_called_with(
|
|
'container', 'object', headers={
|
|
'Content-Type': 'text/plain',
|
|
'X-Object-Meta-Color': 'Blue'})
|
|
|
|
@mock.patch('swiftclient.shell.Connection')
|
|
def test_capabilities(self, connection):
|
|
argv = ["", "capabilities"]
|
|
connection.return_value.get_capabilities.return_value = {'swift': None}
|
|
swiftclient.shell.main(argv)
|
|
connection.return_value.get_capabilities.assert_called_with(None)
|