Merge tag '1.4.0' into debian/liberty
python-cinderclient 1.4.0 release
This commit is contained in:
@@ -238,14 +238,12 @@ class HTTPClient(object):
|
||||
**kwargs)
|
||||
self.http_log_resp(resp)
|
||||
|
||||
body = None
|
||||
if resp.text:
|
||||
try:
|
||||
body = json.loads(resp.text)
|
||||
except ValueError:
|
||||
pass
|
||||
body = None
|
||||
else:
|
||||
body = None
|
||||
except ValueError as e:
|
||||
self._logger.debug("Load http response text error: %s", e)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
@@ -82,7 +82,9 @@ class ClientException(Exception):
|
||||
"""
|
||||
def __init__(self, code, message=None, details=None, request_id=None):
|
||||
self.code = code
|
||||
self.message = message or self.__class__.message
|
||||
# NOTE(mriedem): Use getattr on self.__class__.message since
|
||||
# BaseException.message was dropped in python 3, see PEP 0352.
|
||||
self.message = message or getattr(self.__class__, 'message', None)
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
|
||||
@@ -176,8 +178,8 @@ def from_response(response, body):
|
||||
details = "n/a"
|
||||
if hasattr(body, 'keys'):
|
||||
error = body[list(body)[0]]
|
||||
message = error.get('message', None)
|
||||
details = error.get('details', None)
|
||||
message = error.get('message', message)
|
||||
details = error.get('details', details)
|
||||
return cls(code=response.status_code, message=message, details=details,
|
||||
request_id=request_id)
|
||||
else:
|
||||
|
||||
@@ -52,7 +52,7 @@ import six.moves.urllib.parse as urlparse
|
||||
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
DEFAULT_OS_VOLUME_API_VERSION = "1"
|
||||
DEFAULT_OS_VOLUME_API_VERSION = "2"
|
||||
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
|
||||
DEFAULT_CINDER_SERVICE_TYPE = 'volume'
|
||||
|
||||
@@ -360,18 +360,6 @@ class OpenStackCinderShell(object):
|
||||
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
|
||||
|
||||
parser.add_argument(
|
||||
'--os-cert',
|
||||
metavar='<certificate>',
|
||||
default=utils.env('OS_CERT'),
|
||||
help='Defaults to env[OS_CERT].')
|
||||
|
||||
parser.add_argument(
|
||||
'--os-key',
|
||||
metavar='<key>',
|
||||
default=utils.env('OS_KEY'),
|
||||
help='Defaults to env[OS_KEY].')
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
metavar='<region-name>',
|
||||
default=utils.env('OS_REGION_NAME',
|
||||
@@ -397,19 +385,10 @@ class OpenStackCinderShell(object):
|
||||
'--os_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument(
|
||||
'--os-cacert',
|
||||
metavar='<ca-certificate>',
|
||||
default=utils.env('OS_CACERT', default=None),
|
||||
help=_("Specify a CA bundle file to use in "
|
||||
"verifying a TLS (https) server certificate. "
|
||||
"Defaults to env[OS_CACERT]"))
|
||||
|
||||
parser.add_argument('--insecure',
|
||||
default=utils.env('CINDERCLIENT_INSECURE',
|
||||
default=False),
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
# Register the CLI arguments that have moved to the session object.
|
||||
session.Session.register_cli_options(parser)
|
||||
parser.set_defaults(insecure=utils.env('CINDERCLIENT_INSECURE',
|
||||
default=False))
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
@@ -611,6 +590,14 @@ class OpenStackCinderShell(object):
|
||||
# FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for os_username or os_password but for compatibility it is not.
|
||||
|
||||
# V3 stuff
|
||||
project_info_provided = ((self.options.os_tenant_name or
|
||||
self.options.os_tenant_id) or
|
||||
(self.options.os_project_name and
|
||||
(self.options.os_project_domain_name or
|
||||
self.options.os_project_domain_id)) or
|
||||
self.options.os_project_id)
|
||||
|
||||
if not utils.isunauthenticated(args.func):
|
||||
if auth_plugin:
|
||||
auth_plugin.parse_opts(args)
|
||||
@@ -637,32 +624,20 @@ class OpenStackCinderShell(object):
|
||||
"env[OS_PASSWORD] "
|
||||
"or, prompted response.")
|
||||
|
||||
if not (os_tenant_name or os_tenant_id):
|
||||
raise exc.CommandError("You must provide a tenant ID "
|
||||
"through --os-tenant-id or "
|
||||
"env[OS_TENANT_ID].")
|
||||
|
||||
# V3 stuff
|
||||
project_info_provided = self.options.os_tenant_name or \
|
||||
self.options.os_tenant_id or \
|
||||
(self.options.os_project_name and
|
||||
(self.options.project_domain_name or
|
||||
self.options.project_domain_id)) or \
|
||||
self.options.os_project_id
|
||||
|
||||
if (not project_info_provided):
|
||||
raise exc.CommandError(
|
||||
_("You must provide a tenant_name, tenant_id, "
|
||||
"project_id or project_name (with "
|
||||
"project_domain_name or project_domain_id) via "
|
||||
" --os-tenant-name (env[OS_TENANT_NAME]),"
|
||||
" --os-tenant-id (env[OS_TENANT_ID]),"
|
||||
" --os-project-id (env[OS_PROJECT_ID])"
|
||||
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||
" --os-project-domain-id "
|
||||
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||
" --os-project-domain-name "
|
||||
"(env[OS_PROJECT_DOMAIN_NAME])"))
|
||||
if not project_info_provided:
|
||||
raise exc.CommandError(_(
|
||||
"You must provide a tenant_name, tenant_id, "
|
||||
"project_id or project_name (with "
|
||||
"project_domain_name or project_domain_id) via "
|
||||
" --os-tenant-name (env[OS_TENANT_NAME]),"
|
||||
" --os-tenant-id (env[OS_TENANT_ID]),"
|
||||
" --os-project-id (env[OS_PROJECT_ID])"
|
||||
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||
" --os-project-domain-id "
|
||||
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||
" --os-project-domain-name "
|
||||
"(env[OS_PROJECT_DOMAIN_NAME])"
|
||||
))
|
||||
|
||||
if not os_auth_url:
|
||||
if os_auth_system and os_auth_system != 'keystone':
|
||||
@@ -673,10 +648,20 @@ class OpenStackCinderShell(object):
|
||||
"You must provide an authentication URL "
|
||||
"through --os-auth-url or env[OS_AUTH_URL].")
|
||||
|
||||
if not (os_tenant_name or os_tenant_id):
|
||||
raise exc.CommandError(
|
||||
"You must provide a tenant ID "
|
||||
"through --os-tenant-id or env[OS_TENANT_ID].")
|
||||
if not project_info_provided:
|
||||
raise exc.CommandError(_(
|
||||
"You must provide a tenant_name, tenant_id, "
|
||||
"project_id or project_name (with "
|
||||
"project_domain_name or project_domain_id) via "
|
||||
" --os-tenant-name (env[OS_TENANT_NAME]),"
|
||||
" --os-tenant-id (env[OS_TENANT_ID]),"
|
||||
" --os-project-id (env[OS_PROJECT_ID])"
|
||||
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||
" --os-project-domain-id "
|
||||
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||
" --os-project-domain-name "
|
||||
"(env[OS_PROJECT_DOMAIN_NAME])"
|
||||
))
|
||||
|
||||
if not os_auth_url:
|
||||
raise exc.CommandError(
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from six.moves import configparser
|
||||
import six
|
||||
from tempest_lib.cli import base
|
||||
from tempest_lib.cli import output_parser
|
||||
|
||||
from tempest_lib import exceptions
|
||||
|
||||
_CREDS_FILE = 'functional_creds.conf'
|
||||
|
||||
@@ -36,7 +37,7 @@ def credentials():
|
||||
tenant_name = os.environ.get('OS_TENANT_NAME')
|
||||
auth_url = os.environ.get('OS_AUTH_URL')
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config = six.moves.configparser.RawConfigParser()
|
||||
if config.read(_CREDS_FILE):
|
||||
username = username or config.get('admin', 'user')
|
||||
password = password or config.get('admin', 'pass')
|
||||
@@ -95,3 +96,148 @@ class ClientTestBase(base.ClientTestBase):
|
||||
for item in items:
|
||||
for field in field_names:
|
||||
self.assertIn(field, item)
|
||||
|
||||
def assert_volume_details(self, items):
|
||||
"""Check presence of common volume properties.
|
||||
|
||||
:param items: volume properties
|
||||
"""
|
||||
values = ('attachments', 'availability_zone', 'bootable', 'created_at',
|
||||
'description', 'encrypted', 'id', 'metadata', 'name', 'size',
|
||||
'status', 'user_id', 'volume_type')
|
||||
|
||||
for value in values:
|
||||
self.assertIn(value, items)
|
||||
|
||||
def wait_for_volume_status(self, volume_id, status, timeout=60):
|
||||
"""Wait until volume reaches given status.
|
||||
|
||||
:param volume_id: uuid4 id of given volume
|
||||
:param status: expected status of volume
|
||||
:param timeout: timeout in seconds
|
||||
"""
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if status in self.cinder('show', params=volume_id):
|
||||
break
|
||||
else:
|
||||
self.fail("Volume %s did not reach status %s after %d seconds."
|
||||
% (volume_id, status, timeout))
|
||||
|
||||
def check_volume_not_deleted(self, volume_id):
|
||||
"""Check that volume exists.
|
||||
|
||||
:param volume_id: uuid4 id of given volume
|
||||
"""
|
||||
self.assertTrue(self.cinder('show', params=volume_id))
|
||||
|
||||
def check_volume_deleted(self, volume_id, timeout=60):
|
||||
"""Check that volume deleted successfully.
|
||||
|
||||
:param volume_id: uuid4 id of given volume
|
||||
:param timeout: timeout in seconds
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if volume_id not in self.cinder('show', params=volume_id):
|
||||
break
|
||||
except exceptions.CommandFailed:
|
||||
pass
|
||||
else:
|
||||
self.fail("Volume %s not deleted after %d seconds."
|
||||
% (volume_id, timeout))
|
||||
|
||||
def volume_create(self, params):
|
||||
"""Create volume.
|
||||
|
||||
:param params: parameters to cinder command
|
||||
:return: volume dictionary
|
||||
"""
|
||||
output = self.cinder('create', params=params)
|
||||
volume = self._get_property_from_output(output)
|
||||
self.addCleanup(self.volume_delete, volume['id'])
|
||||
self.wait_for_volume_status(volume['id'], 'available')
|
||||
return volume
|
||||
|
||||
def volume_delete(self, volume_id):
|
||||
"""Delete specified volume by ID.
|
||||
|
||||
:param volume_id: uuid4 id of given volume
|
||||
"""
|
||||
if volume_id in self.cinder('list'):
|
||||
self.cinder('delete', params=volume_id)
|
||||
|
||||
def _get_property_from_output(self, output):
|
||||
"""Create a dictionary from an output
|
||||
|
||||
:param output: the output of the cmd
|
||||
"""
|
||||
obj = {}
|
||||
items = self.parser.listing(output)
|
||||
for item in items:
|
||||
obj[item['Property']] = six.text_type(item['Value'])
|
||||
return obj
|
||||
|
||||
def wait_for_snapshot_status(self, snapshot_id, status, timeout=60):
|
||||
"""Wait until snapshot reaches given status.
|
||||
|
||||
:param snapshot_id: uuid4 id of given volume
|
||||
:param status: expected snapshot's status
|
||||
:param timeout: timeout in seconds
|
||||
"""
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if status in self.cinder('snapshot-show', params=snapshot_id):
|
||||
break
|
||||
else:
|
||||
self.fail("Snapshot %s did not reach status %s after %d seconds."
|
||||
% (snapshot_id, status, timeout))
|
||||
|
||||
def check_snapshot_deleted(self, snapshot_id, timeout=60):
|
||||
"""Check that snapshot deleted successfully.
|
||||
|
||||
:param snapshot_id: the given snapshot id
|
||||
:param timeout: timeout in seconds
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if snapshot_id not in self.cinder('snapshot-show',
|
||||
params=snapshot_id):
|
||||
break
|
||||
except exceptions.CommandFailed:
|
||||
pass
|
||||
else:
|
||||
self.fail("Snapshot %s has not deleted after %d seconds."
|
||||
% (snapshot_id, timeout))
|
||||
|
||||
def assert_snapshot_details(self, items):
|
||||
"""Check presence of common volume snapshot properties.
|
||||
|
||||
:param items: volume snapshot properties
|
||||
"""
|
||||
values = ('created_at', 'description', 'id', 'metadata', 'name',
|
||||
'size', 'status', 'volume_id')
|
||||
|
||||
for value in values:
|
||||
self.assertIn(value, items)
|
||||
|
||||
def snapshot_create(self, volume_id):
|
||||
"""Create a volume snapshot from the volume id.
|
||||
|
||||
:param volume_id: the given volume id to create a snapshot
|
||||
"""
|
||||
output = self.cinder('snapshot-create', params=volume_id)
|
||||
snapshot = self._get_property_from_output(output)
|
||||
self.addCleanup(self.snapshot_delete, snapshot['id'])
|
||||
self.wait_for_snapshot_status(snapshot['id'], 'available')
|
||||
return snapshot
|
||||
|
||||
def snapshot_delete(self, snapshot_id):
|
||||
"""Delete specified snapshot by ID.
|
||||
|
||||
:param snapshot_id: the given snapshot id
|
||||
"""
|
||||
if snapshot_id in self.cinder('snapshot-list'):
|
||||
self.cinder('snapshot-delete', params=snapshot_id)
|
||||
|
||||
@@ -21,7 +21,7 @@ function generate_testr_results {
|
||||
if [ -f .testrepository/0 ]; then
|
||||
sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit
|
||||
sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit
|
||||
sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html
|
||||
sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html
|
||||
sudo gzip -9 $BASE/logs/testrepository.subunit
|
||||
sudo gzip -9 $BASE/logs/testr_results.html
|
||||
sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz
|
||||
|
||||
67
cinderclient/tests/functional/test_cli.py
Normal file
67
cinderclient/tests/functional/test_cli.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 cinderclient.tests.functional import base
|
||||
|
||||
|
||||
class CinderClientTests(base.ClientTestBase):
|
||||
"""Basic test for cinder client.
|
||||
|
||||
Check of base cinder commands.
|
||||
"""
|
||||
def test_volume_create_delete_id(self):
|
||||
"""Create and delete a volume by ID."""
|
||||
volume = self.volume_create(params='1')
|
||||
self.assert_volume_details(volume.keys())
|
||||
self.volume_delete(volume['id'])
|
||||
self.check_volume_deleted(volume['id'])
|
||||
|
||||
def test_volume_create_delete_name(self):
|
||||
"""Create and delete a volume by name."""
|
||||
volume = self.volume_create(params='1 --name TestVolumeNamedCreate')
|
||||
|
||||
self.cinder('delete', params='TestVolumeNamedCreate')
|
||||
self.check_volume_deleted(volume['id'])
|
||||
|
||||
def test_volume_show(self):
|
||||
"""Show volume details."""
|
||||
volume = self.volume_create(params='1 --name TestVolumeShow')
|
||||
output = self.cinder('show', params='TestVolumeShow')
|
||||
volume = self._get_property_from_output(output)
|
||||
self.assertEqual('TestVolumeShow', volume['name'])
|
||||
self.assert_volume_details(volume.keys())
|
||||
|
||||
self.volume_delete(volume['id'])
|
||||
self.check_volume_deleted(volume['id'])
|
||||
|
||||
def test_volume_extend(self):
|
||||
"""Extend a volume size."""
|
||||
volume = self.volume_create(params='1 --name TestVolumeExtend')
|
||||
self.cinder('extend', params="%s %s" % (volume['id'], 2))
|
||||
self.wait_for_volume_status(volume['id'], 'available')
|
||||
output = self.cinder('show', params=volume['id'])
|
||||
volume = self._get_property_from_output(output)
|
||||
self.assertEqual('2', volume['size'])
|
||||
|
||||
self.volume_delete(volume['id'])
|
||||
self.check_volume_deleted(volume['id'])
|
||||
|
||||
def test_snapshot_create_and_delete(self):
|
||||
"""Create a volume snapshot and then delete."""
|
||||
volume = self.volume_create(params='1')
|
||||
snapshot = self.snapshot_create(volume['id'])
|
||||
self.assert_snapshot_details(snapshot.keys())
|
||||
self.snapshot_delete(snapshot['id'])
|
||||
self.check_snapshot_deleted(snapshot['id'])
|
||||
self.volume_delete(volume['id'])
|
||||
self.check_volume_deleted(volume['id'])
|
||||
@@ -53,6 +53,17 @@ def mock_http_request(resp=None):
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "volumev2",
|
||||
"endpoints": [
|
||||
{
|
||||
"region": "RegionOne",
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
32
cinderclient/tests/unit/test_exceptions.py
Normal file
32
cinderclient/tests/unit/test_exceptions.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Tests the cinderclient.exceptions module."""
|
||||
|
||||
import requests
|
||||
|
||||
from cinderclient import exceptions
|
||||
from cinderclient.tests.unit import utils
|
||||
|
||||
|
||||
class ExceptionsTest(utils.TestCase):
|
||||
|
||||
def test_from_response_no_body_message(self):
|
||||
# Tests that we get ClientException back since we don't have 500 mapped
|
||||
response = requests.Response()
|
||||
response.status_code = 500
|
||||
body = {'keys': ({})}
|
||||
ex = exceptions.from_response(response, body)
|
||||
self.assertIs(exceptions.ClientException, type(ex))
|
||||
self.assertEqual('n/a', ex.message)
|
||||
@@ -113,22 +113,26 @@ class ShellTest(utils.TestCase):
|
||||
v2_url, v3_url = _shell._discover_auth_versions(
|
||||
None, auth_url=os_auth_url)
|
||||
self.assertEqual(v2_url, os_auth_url, "Expected v2 url")
|
||||
self.assertEqual(v3_url, None, "Expected no v3 url")
|
||||
self.assertIsNone(v3_url, "Expected no v3 url")
|
||||
|
||||
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v3.0"
|
||||
self.register_keystone_auth_fixture(mocker, os_auth_url)
|
||||
v2_url, v3_url = _shell._discover_auth_versions(
|
||||
None, auth_url=os_auth_url)
|
||||
self.assertEqual(v3_url, os_auth_url, "Expected v3 url")
|
||||
self.assertEqual(v2_url, None, "Expected no v2 url")
|
||||
self.assertIsNone(v2_url, "Expected no v2 url")
|
||||
|
||||
@mock.patch('keystoneclient.adapter.Adapter.get_token',
|
||||
side_effect=ks_exc.ConnectionRefused())
|
||||
@mock.patch('keystoneclient.discover.Discover',
|
||||
side_effect=ks_exc.ConnectionRefused())
|
||||
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
|
||||
@mock.patch('getpass.getpass', return_value='password')
|
||||
def test_password_prompted(self, mock_getpass, mock_stdin):
|
||||
def test_password_prompted(self, mock_getpass, mock_stdin, mock_discover,
|
||||
mock_token):
|
||||
self.make_env(exclude='OS_PASSWORD')
|
||||
# We should get a Connection Refused because there is no keystone.
|
||||
self.assertRaises(ks_exc.ConnectionRefused, self.shell, 'list')
|
||||
# Make sure we are actually prompted.
|
||||
_shell = shell.OpenStackCinderShell()
|
||||
self.assertRaises(ks_exc.ConnectionRefused, _shell.main, ['list'])
|
||||
mock_getpass.assert_called_with('OS Password: ')
|
||||
|
||||
@mock.patch.object(requests, "request")
|
||||
@@ -167,7 +171,7 @@ class ShellTest(utils.TestCase):
|
||||
token_url = _shell.cs.client.auth_url + "/tokens"
|
||||
self.assertEqual(non_keystone_auth_url + "/tokens", token_url)
|
||||
|
||||
mock_request.assert_any_called(
|
||||
mock_request.assert_any_call(
|
||||
"POST",
|
||||
token_url,
|
||||
headers=headers,
|
||||
|
||||
@@ -203,4 +203,38 @@ class PrintListTestCase(test_utils.TestCase):
|
||||
| 1 | 2 |
|
||||
| 3 | 4 |
|
||||
+---+---+
|
||||
""", cso.read())
|
||||
|
||||
def test_print_list_with_return(self):
|
||||
Row = collections.namedtuple('Row', ['a', 'b'])
|
||||
to_print = [Row(a=3, b='a\r'), Row(a=1, b='c\rd')]
|
||||
with CaptureStdout() as cso:
|
||||
utils.print_list(to_print, ['a', 'b'])
|
||||
# Output should be sorted by the first key (a)
|
||||
self.assertEqual("""\
|
||||
+---+-----+
|
||||
| a | b |
|
||||
+---+-----+
|
||||
| 1 | c d |
|
||||
| 3 | a |
|
||||
+---+-----+
|
||||
""", cso.read())
|
||||
|
||||
|
||||
class PrintDictTestCase(test_utils.TestCase):
|
||||
|
||||
def test_print_dict_with_return(self):
|
||||
d = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'test\rcarriage\n\rreturn'}
|
||||
with CaptureStdout() as cso:
|
||||
utils.print_dict(d)
|
||||
self.assertEqual("""\
|
||||
+----------+---------------+
|
||||
| Property | Value |
|
||||
+----------+---------------+
|
||||
| a | A |
|
||||
| b | B |
|
||||
| c | C |
|
||||
| d | test carriage |
|
||||
| | return |
|
||||
+----------+---------------+
|
||||
""", cso.read())
|
||||
|
||||
@@ -322,8 +322,8 @@ class AuthenticationTests(utils.TestCase):
|
||||
@mock.patch.object(http_client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
http_client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
self.assertTrue(m.called)
|
||||
self.assertTrue(mock_request.called)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
@@ -333,6 +333,6 @@ class AuthenticationTests(utils.TestCase):
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
self.assertTrue(m.called)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
@@ -449,6 +449,10 @@ class FakeHTTPClient(base_client.HTTPClient):
|
||||
assert body[action] is None
|
||||
elif action == 'os-reenable-replica':
|
||||
assert body[action] is None
|
||||
elif action == 'os-set_image_metadata':
|
||||
assert list(body[action]) == ['metadata']
|
||||
elif action == 'os-unset_image_metadata':
|
||||
assert 'key' in body[action]
|
||||
else:
|
||||
raise AssertionError("Unexpected action: %s" % action)
|
||||
return (resp, {}, _body)
|
||||
@@ -1063,3 +1067,20 @@ class FakeHTTPClient(base_client.HTTPClient):
|
||||
},
|
||||
]
|
||||
return (200, {}, {"pools": stats})
|
||||
|
||||
def get_capabilities_host(self, **kw):
|
||||
return (200, {},
|
||||
{
|
||||
'namespace': 'OS::Storage::Capabilities::fake',
|
||||
'vendor_name': 'OpenStack',
|
||||
'volume_backend_name': 'lvm',
|
||||
'pool_name': 'pool',
|
||||
'storage_protocol': 'iSCSI',
|
||||
'properties': {
|
||||
'compression': {
|
||||
'title': 'Compression',
|
||||
'description': 'Enables compression.',
|
||||
'type': 'boolean'},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -325,8 +325,8 @@ class AuthenticationTests(utils.TestCase):
|
||||
@mock.patch.object(http_client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
http_client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
self.assertTrue(m.called)
|
||||
self.assertTrue(mock_request.called)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
@@ -336,6 +336,6 @@ class AuthenticationTests(utils.TestCase):
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
self.assertTrue(m.called)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
41
cinderclient/tests/unit/v2/test_capabilities.py
Normal file
41
cinderclient/tests/unit/v2/test_capabilities.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2015 Hitachi Data Systems, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 cinderclient.tests.unit import utils
|
||||
from cinderclient.tests.unit.v2 import fakes
|
||||
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
class CapabilitiesTest(utils.TestCase):
|
||||
|
||||
def test_get_capabilities(self):
|
||||
expected = {
|
||||
'namespace': 'OS::Storage::Capabilities::fake',
|
||||
'vendor_name': 'OpenStack',
|
||||
'volume_backend_name': 'lvm',
|
||||
'pool_name': 'pool',
|
||||
'storage_protocol': 'iSCSI',
|
||||
'properties': {
|
||||
'compression': {
|
||||
'title': 'Compression',
|
||||
'description': 'Enables compression.',
|
||||
'type': 'boolean'},
|
||||
}
|
||||
}
|
||||
|
||||
capabilities = cs.capabilities.get('host')
|
||||
cs.assert_called('GET', '/capabilities/host')
|
||||
self.assertEqual(expected, capabilities._info)
|
||||
@@ -89,13 +89,13 @@ class ConsistencygroupsTest(utils.TestCase):
|
||||
cs.assert_called('PUT', '/consistencygroups/1234', body=expected)
|
||||
|
||||
def test_update_consistencygroup_none(self):
|
||||
self.assertEqual(None, cs.consistencygroups.update('1234'))
|
||||
self.assertIsNone(cs.consistencygroups.update('1234'))
|
||||
|
||||
def test_update_consistencygroup_no_props(self):
|
||||
cs.consistencygroups.update('1234')
|
||||
|
||||
def test_create_consistencygroup_from_src(self):
|
||||
cs.consistencygroups.create_from_src('5678', name='cg')
|
||||
def test_create_consistencygroup_from_src_snap(self):
|
||||
cs.consistencygroups.create_from_src('5678', None, name='cg')
|
||||
expected = {
|
||||
'consistencygroup-from-src': {
|
||||
'status': 'creating',
|
||||
@@ -103,7 +103,24 @@ class ConsistencygroupsTest(utils.TestCase):
|
||||
'user_id': None,
|
||||
'name': 'cg',
|
||||
'cgsnapshot_id': '5678',
|
||||
'project_id': None
|
||||
'project_id': None,
|
||||
'source_cgid': None
|
||||
}
|
||||
}
|
||||
cs.assert_called('POST', '/consistencygroups/create_from_src',
|
||||
body=expected)
|
||||
|
||||
def test_create_consistencygroup_from_src_cg(self):
|
||||
cs.consistencygroups.create_from_src(None, '5678', name='cg')
|
||||
expected = {
|
||||
'consistencygroup-from-src': {
|
||||
'status': 'creating',
|
||||
'description': None,
|
||||
'user_id': None,
|
||||
'name': 'cg',
|
||||
'source_cgid': '5678',
|
||||
'project_id': None,
|
||||
'cgsnapshot_id': None
|
||||
}
|
||||
}
|
||||
cs.assert_called('POST', '/consistencygroups/create_from_src',
|
||||
|
||||
@@ -21,6 +21,8 @@ from six.moves.urllib import parse
|
||||
from cinderclient import client
|
||||
from cinderclient import exceptions
|
||||
from cinderclient import shell
|
||||
from cinderclient.v2 import volumes
|
||||
from cinderclient.v2 import shell as test_shell
|
||||
from cinderclient.tests.unit import utils
|
||||
from cinderclient.tests.unit.v2 import fakes
|
||||
from cinderclient.tests.unit.fixture_data import keystone_client
|
||||
@@ -55,6 +57,15 @@ class ShellTest(utils.TestCase):
|
||||
'GET', keystone_client.BASE_URL,
|
||||
text=keystone_client.keystone_request_callback)
|
||||
|
||||
self.cs = mock.Mock()
|
||||
|
||||
def _make_args(self, args):
|
||||
class Args(object):
|
||||
def __init__(self, entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
return Args(args)
|
||||
|
||||
def tearDown(self):
|
||||
# For some methods like test_image_meta_bad_action we are
|
||||
# testing a SystemExit to be thrown and object self.shell has
|
||||
@@ -265,7 +276,8 @@ class ShellTest(utils.TestCase):
|
||||
with mock.patch('cinderclient.utils.print_list') as mock_print:
|
||||
self.run_command(cmd)
|
||||
mock_print.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, sortby_index=None)
|
||||
mock.ANY, mock.ANY, exclude_unavailable=True,
|
||||
sortby_index=None)
|
||||
|
||||
def test_list_reorder_without_sort(self):
|
||||
# sortby_index is 0 without sort information
|
||||
@@ -273,7 +285,8 @@ class ShellTest(utils.TestCase):
|
||||
with mock.patch('cinderclient.utils.print_list') as mock_print:
|
||||
self.run_command(cmd)
|
||||
mock_print.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, sortby_index=0)
|
||||
mock.ANY, mock.ANY, exclude_unavailable=True,
|
||||
sortby_index=0)
|
||||
|
||||
def test_list_availability_zone(self):
|
||||
self.run_command('availability-zone-list')
|
||||
@@ -359,10 +372,38 @@ class ShellTest(utils.TestCase):
|
||||
self.run_command('backup-create 1234 --incremental')
|
||||
self.assert_called('POST', '/backups')
|
||||
|
||||
def test_backup_force(self):
|
||||
self.run_command('backup-create 1234 --force')
|
||||
self.assert_called('POST', '/backups')
|
||||
|
||||
def test_restore(self):
|
||||
self.run_command('backup-restore 1234')
|
||||
self.assert_called('POST', '/backups/1234/restore')
|
||||
|
||||
@mock.patch('cinderclient.utils.print_dict')
|
||||
@mock.patch('cinderclient.utils.find_volume')
|
||||
def test_do_backup_restore(self,
|
||||
mock_find_volume,
|
||||
mock_print_dict):
|
||||
backup_id = '1234'
|
||||
volume_id = '5678'
|
||||
input = {
|
||||
'backup': backup_id,
|
||||
'volume': volume_id
|
||||
}
|
||||
|
||||
args = self._make_args(input)
|
||||
with mock.patch.object(self.cs.restores,
|
||||
'restore') as mocked_restore:
|
||||
mock_find_volume.return_value = volumes.Volume(self,
|
||||
{'id': volume_id},
|
||||
loaded = True)
|
||||
test_shell.do_backup_restore(self.cs, args)
|
||||
mocked_restore.assert_called_once_with(
|
||||
input['backup'],
|
||||
volume_id)
|
||||
self.assertTrue(mock_print_dict.called)
|
||||
|
||||
def test_record_export(self):
|
||||
self.run_command('backup-export 1234')
|
||||
self.assert_called('GET', '/backups/1234/export_record')
|
||||
@@ -455,6 +496,25 @@ class ShellTest(utils.TestCase):
|
||||
expected = {'os-reset_status': {'status': 'error'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_reset_state_with_attach_status(self):
|
||||
self.run_command('reset-state --attach-status detached 1234')
|
||||
expected = {'os-reset_status': {'status': 'available',
|
||||
'attach_status': 'detached'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_reset_state_with_attach_status_with_flag(self):
|
||||
self.run_command('reset-state --state in-use '
|
||||
'--attach-status attached 1234')
|
||||
expected = {'os-reset_status': {'status': 'in-use',
|
||||
'attach_status': 'attached'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_reset_state_with_reset_migration_status(self):
|
||||
self.run_command('reset-state --reset-migration-status 1234')
|
||||
expected = {'os-reset_status': {'status': 'available',
|
||||
'migration_status': 'none'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_reset_state_multiple(self):
|
||||
self.run_command('reset-state 1234 5678 --state error')
|
||||
expected = {'os-reset_status': {'status': 'error'}}
|
||||
@@ -671,17 +731,38 @@ class ShellTest(utils.TestCase):
|
||||
self.assert_called_anytime('GET', '/types/1')
|
||||
|
||||
def test_migrate_volume(self):
|
||||
self.run_command('migrate 1234 fakehost --force-host-copy=True')
|
||||
self.run_command('migrate 1234 fakehost --force-host-copy=True '
|
||||
'--lock-volume=True')
|
||||
expected = {'os-migrate_volume': {'force_host_copy': 'True',
|
||||
'lock_volume': 'True',
|
||||
'host': 'fakehost'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_migrate_volume_bool_force(self):
|
||||
self.run_command('migrate 1234 fakehost --force-host-copy')
|
||||
self.run_command('migrate 1234 fakehost --force-host-copy '
|
||||
'--lock-volume')
|
||||
expected = {'os-migrate_volume': {'force_host_copy': True,
|
||||
'lock_volume': True,
|
||||
'host': 'fakehost'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
def test_migrate_volume_bool_force_false(self):
|
||||
# Set both --force-host-copy and --lock-volume to False.
|
||||
self.run_command('migrate 1234 fakehost --force-host-copy=False '
|
||||
'--lock-volume=False')
|
||||
expected = {'os-migrate_volume': {'force_host_copy': 'False',
|
||||
'lock_volume': 'False',
|
||||
'host': 'fakehost'}}
|
||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||
|
||||
# Do not set the values to --force-host-copy and --lock-volume.
|
||||
self.run_command('migrate 1234 fakehost')
|
||||
expected = {'os-migrate_volume': {'force_host_copy': False,
|
||||
'lock_volume': False,
|
||||
'host': 'fakehost'}}
|
||||
self.assert_called('POST', '/volumes/1234/action',
|
||||
body=expected)
|
||||
|
||||
def test_snapshot_metadata_set(self):
|
||||
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
|
||||
self.assert_called('POST', '/snapshots/1234/metadata',
|
||||
@@ -913,7 +994,7 @@ class ShellTest(utils.TestCase):
|
||||
self.run_command,
|
||||
'consisgroup-update 1234')
|
||||
|
||||
def test_consistencygroup_create_from_src(self):
|
||||
def test_consistencygroup_create_from_src_snap(self):
|
||||
self.run_command('consisgroup-create-from-src '
|
||||
'--name cg '
|
||||
'--cgsnapshot 1234')
|
||||
@@ -924,14 +1005,89 @@ class ShellTest(utils.TestCase):
|
||||
'description': None,
|
||||
'user_id': None,
|
||||
'project_id': None,
|
||||
'status': 'creating'
|
||||
'status': 'creating',
|
||||
'source_cgid': None
|
||||
}
|
||||
}
|
||||
self.assert_called('POST', '/consistencygroups/create_from_src',
|
||||
expected)
|
||||
|
||||
def test_consistencygroup_create_from_src_bad_request(self):
|
||||
def test_consistencygroup_create_from_src_cg(self):
|
||||
self.run_command('consisgroup-create-from-src '
|
||||
'--name cg '
|
||||
'--source-cg 1234')
|
||||
expected = {
|
||||
'consistencygroup-from-src': {
|
||||
'name': 'cg',
|
||||
'cgsnapshot_id': None,
|
||||
'description': None,
|
||||
'user_id': None,
|
||||
'project_id': None,
|
||||
'status': 'creating',
|
||||
'source_cgid': '1234'
|
||||
}
|
||||
}
|
||||
self.assert_called('POST', '/consistencygroups/create_from_src',
|
||||
expected)
|
||||
|
||||
def test_consistencygroup_create_from_src_fail_no_snap_cg(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.run_command,
|
||||
'consisgroup-create-from-src '
|
||||
'--name cg')
|
||||
|
||||
def test_consistencygroup_create_from_src_fail_both_snap_cg(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.run_command,
|
||||
'consisgroup-create-from-src '
|
||||
'--name cg '
|
||||
'--cgsnapshot 1234 '
|
||||
'--source-cg 5678')
|
||||
|
||||
def test_set_image_metadata(self):
|
||||
self.run_command('image-metadata 1234 set key1=val1')
|
||||
expected = {"os-set_image_metadata": {"metadata": {"key1": "val1"}}}
|
||||
self.assert_called('POST', '/volumes/1234/action',
|
||||
body=expected)
|
||||
|
||||
def test_unset_image_metadata(self):
|
||||
self.run_command('image-metadata 1234 unset key1')
|
||||
expected = {"os-unset_image_metadata": {"key": "key1"}}
|
||||
self.assert_called('POST', '/volumes/1234/action',
|
||||
body=expected)
|
||||
|
||||
def _get_params_from_stack(self, pos=-1):
|
||||
method, url = self.shell.cs.client.callstack[pos][0:2]
|
||||
path, query = parse.splitquery(url)
|
||||
params = parse.parse_qs(query)
|
||||
return path, params
|
||||
|
||||
def test_backup_list_all_tenants(self):
|
||||
self.run_command('backup-list --all-tenants=1 --name=bc '
|
||||
'--status=available --volume-id=1234')
|
||||
expected = {
|
||||
'all_tenants': ['1'],
|
||||
'name': ['bc'],
|
||||
'status': ['available'],
|
||||
'volume_id': ['1234'],
|
||||
}
|
||||
|
||||
path, params = self._get_params_from_stack()
|
||||
|
||||
self.assertEqual('/backups/detail', path)
|
||||
self.assertEqual(4, len(params))
|
||||
|
||||
for k in params.keys():
|
||||
self.assertEqual(expected[k], params[k])
|
||||
|
||||
def test_backup_list_volume_id(self):
|
||||
self.run_command('backup-list --volume-id=1234')
|
||||
self.assert_called('GET', '/backups/detail?volume_id=1234')
|
||||
|
||||
def test_backup_list(self):
|
||||
self.run_command('backup-list')
|
||||
self.assert_called('GET', '/backups/detail')
|
||||
|
||||
def test_get_capabilities(self):
|
||||
self.run_command('get-capabilities host')
|
||||
self.assert_called('GET', '/capabilities/host')
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
from cinderclient.tests.unit import utils
|
||||
from cinderclient.tests.unit.v2 import fakes
|
||||
from cinderclient.v2 import volume_backups_restore
|
||||
|
||||
|
||||
cs = fakes.FakeClient()
|
||||
@@ -36,6 +37,11 @@ class VolumeBackupsTest(utils.TestCase):
|
||||
None, None, True)
|
||||
cs.assert_called('POST', '/backups')
|
||||
|
||||
def test_create_force(self):
|
||||
cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4',
|
||||
None, None, False, True)
|
||||
cs.assert_called('POST', '/backups')
|
||||
|
||||
def test_get(self):
|
||||
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
|
||||
cs.backups.get(backup_id)
|
||||
@@ -59,8 +65,10 @@ class VolumeBackupsTest(utils.TestCase):
|
||||
|
||||
def test_restore(self):
|
||||
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
|
||||
cs.restores.restore(backup_id)
|
||||
info = cs.restores.restore(backup_id)
|
||||
cs.assert_called('POST', '/backups/%s/restore' % backup_id)
|
||||
self.assertIsInstance(info,
|
||||
volume_backups_restore.VolumeBackupsRestore)
|
||||
|
||||
def test_record_export(self):
|
||||
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
|
||||
|
||||
@@ -160,14 +160,31 @@ class VolumesTest(utils.TestCase):
|
||||
cs.volumes.extend(v, 2)
|
||||
cs.assert_called('POST', '/volumes/1234/action')
|
||||
|
||||
def test_reset_state(self):
|
||||
v = cs.volumes.get('1234')
|
||||
cs.volumes.reset_state(v, 'in-use', attach_status='detached',
|
||||
migration_status='none')
|
||||
cs.assert_called('POST', '/volumes/1234/action')
|
||||
|
||||
def test_get_encryption_metadata(self):
|
||||
cs.volumes.get_encryption_metadata('1234')
|
||||
cs.assert_called('GET', '/volumes/1234/encryption')
|
||||
|
||||
def test_migrate(self):
|
||||
v = cs.volumes.get('1234')
|
||||
cs.volumes.migrate_volume(v, 'dest', False)
|
||||
cs.assert_called('POST', '/volumes/1234/action')
|
||||
cs.volumes.migrate_volume(v, 'dest', False, False)
|
||||
cs.assert_called('POST', '/volumes/1234/action',
|
||||
{'os-migrate_volume': {'host': 'dest',
|
||||
'force_host_copy': False,
|
||||
'lock_volume': False}})
|
||||
|
||||
def test_migrate_with_lock_volume(self):
|
||||
v = cs.volumes.get('1234')
|
||||
cs.volumes.migrate_volume(v, 'dest', False, True)
|
||||
cs.assert_called('POST', '/volumes/1234/action',
|
||||
{'os-migrate_volume': {'host': 'dest',
|
||||
'force_host_copy': False,
|
||||
'lock_volume': True}})
|
||||
|
||||
def test_metadata_update_all(self):
|
||||
cs.volumes.update_all_metadata(1234, {'k1': 'v1'})
|
||||
@@ -235,7 +252,7 @@ class FormatSortParamTestCase(utils.TestCase):
|
||||
|
||||
def test_format_sort_empty_input(self):
|
||||
for s in [None, '', []]:
|
||||
self.assertEqual(None, cs.volumes._format_sort_param(s))
|
||||
self.assertIsNone(cs.volumes._format_sort_param(s))
|
||||
|
||||
def test_format_sort_string_single_key(self):
|
||||
s = 'id'
|
||||
|
||||
@@ -110,11 +110,14 @@ def _print(pt, order):
|
||||
print(strutils.safe_encode(pt.get_string(sortby=order)))
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0):
|
||||
def print_list(objs, fields, exclude_unavailable=False, formatters=None,
|
||||
sortby_index=0):
|
||||
'''Prints a list of objects.
|
||||
|
||||
@param objs: Objects to print
|
||||
@param fields: Fields on each object to be printed
|
||||
@param exclude_unavailable: Boolean to decide if unavailable fields are
|
||||
removed
|
||||
@param formatters: Custom field formatters
|
||||
@param sortby_index: Results sorted against the key in the fields list at
|
||||
this index; if None then the object order is not
|
||||
@@ -122,12 +125,14 @@ def print_list(objs, fields, formatters=None, sortby_index=0):
|
||||
'''
|
||||
formatters = formatters or {}
|
||||
mixed_case_fields = ['serverId']
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
removed_fields = []
|
||||
rows = []
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in removed_fields:
|
||||
continue
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
@@ -138,10 +143,24 @@ def print_list(objs, fields, formatters=None, sortby_index=0):
|
||||
if type(o) == dict and field in o:
|
||||
data = o[field]
|
||||
else:
|
||||
data = getattr(o, field_name, '')
|
||||
if not hasattr(o, field_name) and exclude_unavailable:
|
||||
removed_fields.append(field)
|
||||
continue
|
||||
else:
|
||||
data = getattr(o, field_name, '')
|
||||
if data is None:
|
||||
data = '-'
|
||||
if isinstance(data, six.string_types) and "\r" in data:
|
||||
data = data.replace("\r", " ")
|
||||
row.append(data)
|
||||
rows.append(row)
|
||||
|
||||
for f in removed_fields:
|
||||
fields.remove(f)
|
||||
|
||||
pt = prettytable.PrettyTable((f for f in fields), caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
for row in rows:
|
||||
pt.add_row(row)
|
||||
|
||||
if sortby_index is None:
|
||||
@@ -154,7 +173,11 @@ def print_list(objs, fields, formatters=None, sortby_index=0):
|
||||
def print_dict(d, property="Property"):
|
||||
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
|
||||
pt.aligns = ['l', 'l']
|
||||
[pt.add_row(list(r)) for r in six.iteritems(d)]
|
||||
for r in six.iteritems(d):
|
||||
r = list(r)
|
||||
if isinstance(r[1], six.string_types) and "\r" in r[1]:
|
||||
r[1] = r[1].replace("\r", " ")
|
||||
pt.add_row(r)
|
||||
_print(pt, property)
|
||||
|
||||
|
||||
|
||||
@@ -315,6 +315,7 @@ def do_delete(cs, args):
|
||||
for volume in args.volume:
|
||||
try:
|
||||
utils.find_volume(cs, volume).delete()
|
||||
print("Request to delete volume %s has been accepted." % (volume))
|
||||
except Exception as e:
|
||||
failure_count += 1
|
||||
print("Delete for volume %s failed: %s" % (volume, e))
|
||||
@@ -346,8 +347,8 @@ def do_force_delete(cs, args):
|
||||
'Separate multiple volumes with a space.')
|
||||
@utils.arg('--state', metavar='<state>', default='available',
|
||||
help=('The state to assign to the volume. Valid values are '
|
||||
'"available," "error," "creating," "deleting," "in-use," '
|
||||
'"attaching," "detaching" and "error_deleting." '
|
||||
'"available", "error", "creating", "deleting", "in-use", '
|
||||
'"attaching", "detaching" and "error_deleting". '
|
||||
'NOTE: This command simply changes the state of the '
|
||||
'Volume in the DataBase with no regard to actual status, '
|
||||
'exercise caution when using. Default=available.'))
|
||||
@@ -558,8 +559,8 @@ def do_snapshot_rename(cs, args):
|
||||
help='Name or ID of snapshot to modify.')
|
||||
@utils.arg('--state', metavar='<state>', default='available',
|
||||
help=('The state to assign to the snapshot. Valid values are '
|
||||
'"available," "error," "creating," "deleting," and '
|
||||
'"error_deleting." NOTE: This command simply changes '
|
||||
'"available", "error", "creating", "deleting", and '
|
||||
'"error_deleting". NOTE: This command simply changes '
|
||||
'the state of the Snapshot in the DataBase with no regard '
|
||||
'to actual status, exercise caution when using. '
|
||||
'Default=available.'))
|
||||
|
||||
39
cinderclient/v2/capabilities.py
Normal file
39
cinderclient/v2/capabilities.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) 2015 Hitachi Data Systems, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Capabilities interface (v2 extension)"""
|
||||
|
||||
|
||||
from cinderclient import base
|
||||
|
||||
|
||||
class Capabilities(base.Resource):
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __repr__(self):
|
||||
return "<Capabilities: %s>" % self.name
|
||||
|
||||
|
||||
class CapabilitiesManager(base.Manager):
|
||||
"""Manage :class:`Capabilities` resources."""
|
||||
resource_class = Capabilities
|
||||
|
||||
def get(self, host):
|
||||
"""Show backend volume stats and properties.
|
||||
|
||||
:param host: Specified backend to obtain volume stats and properties.
|
||||
:rtype: :class:`Capabilities`
|
||||
"""
|
||||
return self._get('/capabilities/%s' % host, None)
|
||||
@@ -17,6 +17,7 @@ from cinderclient import client
|
||||
from cinderclient.v2 import availability_zones
|
||||
from cinderclient.v2 import cgsnapshots
|
||||
from cinderclient.v2 import consistencygroups
|
||||
from cinderclient.v2 import capabilities
|
||||
from cinderclient.v2 import limits
|
||||
from cinderclient.v2 import pools
|
||||
from cinderclient.v2 import qos_specs
|
||||
@@ -80,6 +81,7 @@ class Client(object):
|
||||
self.availability_zones = \
|
||||
availability_zones.AvailabilityZoneManager(self)
|
||||
self.pools = pools.PoolManager(self)
|
||||
self.capabilities = capabilities.CapabilitiesManager(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
|
||||
@@ -67,12 +67,13 @@ class ConsistencygroupManager(base.ManagerWithFind):
|
||||
|
||||
return self._create('/consistencygroups', body, 'consistencygroup')
|
||||
|
||||
def create_from_src(self, cgsnapshot_id, name=None,
|
||||
def create_from_src(self, cgsnapshot_id, source_cgid, name=None,
|
||||
description=None, user_id=None,
|
||||
project_id=None):
|
||||
"""Creates a consistencygroup from a cgsnapshot.
|
||||
"""Creates a consistencygroup from a cgsnapshot or a source CG.
|
||||
|
||||
:param cgsnapshot_id: UUID of a CGSnapshot
|
||||
:param source_cgid: UUID of a source CG
|
||||
:param name: Name of the ConsistencyGroup
|
||||
:param description: Description of the ConsistencyGroup
|
||||
:param user_id: User id derived from context
|
||||
@@ -82,6 +83,7 @@ class ConsistencygroupManager(base.ManagerWithFind):
|
||||
body = {'consistencygroup-from-src': {'name': name,
|
||||
'description': description,
|
||||
'cgsnapshot_id': cgsnapshot_id,
|
||||
'source_cgid': source_cgid,
|
||||
'user_id': user_id,
|
||||
'project_id': project_id,
|
||||
'status': "creating",
|
||||
|
||||
@@ -160,6 +160,11 @@ def _extract_metadata(args):
|
||||
metavar='<status>',
|
||||
default=None,
|
||||
help='Filters results by a status. Default=None.')
|
||||
@utils.arg('--migration_status',
|
||||
metavar='<migration_status>',
|
||||
default=None,
|
||||
help='Filters results by a migration status. Default=None. '
|
||||
'Admin only.')
|
||||
@utils.arg('--metadata',
|
||||
type=str,
|
||||
nargs='*',
|
||||
@@ -212,6 +217,7 @@ def do_list(cs, args):
|
||||
'project_id': args.tenant,
|
||||
'name': args.name,
|
||||
'status': args.status,
|
||||
'migration_status': args.migration_status,
|
||||
'metadata': _extract_metadata(args) if args.metadata else None,
|
||||
}
|
||||
|
||||
@@ -233,18 +239,19 @@ def do_list(cs, args):
|
||||
setattr(vol, 'attached_to', ','.join(map(str, servers)))
|
||||
|
||||
if all_tenants:
|
||||
key_list = ['ID', 'Tenant ID', 'Status', 'Name',
|
||||
key_list = ['ID', 'Tenant ID', 'Status', 'Migration Status', 'Name',
|
||||
'Size', 'Volume Type', 'Bootable', 'Multiattach',
|
||||
'Attached to']
|
||||
else:
|
||||
key_list = ['ID', 'Status', 'Name',
|
||||
key_list = ['ID', 'Status', 'Migration Status', 'Name',
|
||||
'Size', 'Volume Type', 'Bootable',
|
||||
'Multiattach', 'Attached to']
|
||||
if args.sort_key or args.sort_dir or args.sort:
|
||||
sortby_index = None
|
||||
else:
|
||||
sortby_index = 0
|
||||
utils.print_list(volumes, key_list, sortby_index=sortby_index)
|
||||
utils.print_list(volumes, key_list, exclude_unavailable=True,
|
||||
sortby_index=sortby_index)
|
||||
|
||||
|
||||
@utils.arg('volume',
|
||||
@@ -420,6 +427,7 @@ def do_delete(cs, args):
|
||||
for volume in args.volume:
|
||||
try:
|
||||
utils.find_volume(cs, volume).delete()
|
||||
print("Request to delete volume %s has been accepted." % (volume))
|
||||
except Exception as e:
|
||||
failure_count += 1
|
||||
print("Delete for volume %s failed: %s" % (volume, e))
|
||||
@@ -450,11 +458,22 @@ def do_force_delete(cs, args):
|
||||
help='Name or ID of volume to modify.')
|
||||
@utils.arg('--state', metavar='<state>', default='available',
|
||||
help=('The state to assign to the volume. Valid values are '
|
||||
'"available," "error," "creating," "deleting," "in-use," '
|
||||
'"attaching," "detaching" and "error_deleting." '
|
||||
'"available", "error", "creating", "deleting", "in-use", '
|
||||
'"attaching", "detaching", "error_deleting" and '
|
||||
'"maintenance". '
|
||||
'NOTE: This command simply changes the state of the '
|
||||
'Volume in the DataBase with no regard to actual status, '
|
||||
'exercise caution when using. Default=available.'))
|
||||
@utils.arg('--attach-status', metavar='<attach-status>', default=None,
|
||||
help=('The attach status to assign to the volume in the DataBase, '
|
||||
'with no regard to the actual status. Valid values are '
|
||||
'"attached" and "detached". Default=None, that means the '
|
||||
'status is unchanged.'))
|
||||
@utils.arg('--reset-migration-status',
|
||||
action='store_true',
|
||||
help=('Clears the migration status of the volume in the DataBase '
|
||||
'that indicates the volume is source or destination of '
|
||||
'volume migration, with no regard to the actual status.'))
|
||||
@utils.service_type('volumev2')
|
||||
def do_reset_state(cs, args):
|
||||
"""Explicitly updates the volume state in the Cinder database.
|
||||
@@ -466,10 +485,13 @@ def do_reset_state(cs, args):
|
||||
unusable in the case of change to the 'available' state.
|
||||
"""
|
||||
failure_flag = False
|
||||
migration_status = 'none' if args.reset_migration_status else None
|
||||
|
||||
for volume in args.volume:
|
||||
try:
|
||||
utils.find_volume(cs, volume).reset_state(args.state)
|
||||
utils.find_volume(cs, volume).reset_state(args.state,
|
||||
args.attach_status,
|
||||
migration_status)
|
||||
except Exception as e:
|
||||
failure_flag = True
|
||||
msg = "Reset state for volume %s failed: %s" % (volume, e)
|
||||
@@ -540,6 +562,32 @@ def do_metadata(cs, args):
|
||||
reverse=True))
|
||||
|
||||
|
||||
@utils.arg('volume',
|
||||
metavar='<volume>',
|
||||
help='Name or ID of volume for which to update metadata.')
|
||||
@utils.arg('action',
|
||||
metavar='<action>',
|
||||
choices=['set', 'unset'],
|
||||
help="The action. Valid values are 'set' or 'unset.'")
|
||||
@utils.arg('metadata',
|
||||
metavar='<key=value>',
|
||||
nargs='+',
|
||||
default=[],
|
||||
help='Metadata key and value pair to set or unset. '
|
||||
'For unset, specify only the key.')
|
||||
@utils.service_type('volumev2')
|
||||
def do_image_metadata(cs, args):
|
||||
"""Sets or deletes volume image metadata."""
|
||||
volume = utils.find_volume(cs, args.volume)
|
||||
metadata = _extract_metadata(args)
|
||||
|
||||
if args.action == 'set':
|
||||
cs.volumes.set_image_metadata(volume, metadata)
|
||||
elif args.action == 'unset':
|
||||
cs.volumes.delete_image_metadata(volume, sorted(metadata.keys(),
|
||||
reverse=True))
|
||||
|
||||
|
||||
@utils.arg('--all-tenants',
|
||||
dest='all_tenants',
|
||||
metavar='<0|1>',
|
||||
@@ -714,8 +762,8 @@ def do_snapshot_rename(cs, args):
|
||||
@utils.arg('--state', metavar='<state>',
|
||||
default='available',
|
||||
help=('The state to assign to the snapshot. Valid values are '
|
||||
'"available," "error," "creating," "deleting," and '
|
||||
'"error_deleting." NOTE: This command simply changes '
|
||||
'"available", "error", "creating", "deleting", and '
|
||||
'"error_deleting". NOTE: This command simply changes '
|
||||
'the state of the Snapshot in the DataBase with no regard '
|
||||
'to actual status, exercise caution when using. '
|
||||
'Default=available.'))
|
||||
@@ -1126,11 +1174,31 @@ def do_upload_to_image(cs, args):
|
||||
help='Enables or disables generic host-based '
|
||||
'force-migration, which bypasses driver '
|
||||
'optimizations. Default=False.')
|
||||
@utils.arg('--lock-volume', metavar='<True|False>',
|
||||
choices=['True', 'False'],
|
||||
required=False,
|
||||
const=True,
|
||||
nargs='?',
|
||||
default=False,
|
||||
help='Enables or disables the termination of volume migration '
|
||||
'caused by other commands. This option applies to the '
|
||||
'available volume. True means it locks the volume '
|
||||
'state and does not allow the migration to be aborted. The '
|
||||
'volume status will be in maintenance during the '
|
||||
'migration. False means it allows the volume migration '
|
||||
'to be aborted. The volume status is still in the original '
|
||||
'status. Default=False.')
|
||||
@utils.service_type('volumev2')
|
||||
def do_migrate(cs, args):
|
||||
"""Migrates volume to a new host."""
|
||||
volume = utils.find_volume(cs, args.volume)
|
||||
volume.migrate_volume(args.host, args.force_host_copy)
|
||||
try:
|
||||
volume.migrate_volume(args.host, args.force_host_copy,
|
||||
args.lock_volume)
|
||||
print("Request to migrate volume %s has been accepted." % (volume))
|
||||
except Exception as e:
|
||||
print("Migration for volume %s failed: %s." % (volume,
|
||||
six.text_type(e)))
|
||||
|
||||
|
||||
@utils.arg('volume', metavar='<volume>',
|
||||
@@ -1166,6 +1234,15 @@ def do_retype(cs, args):
|
||||
action='store_true',
|
||||
help='Incremental backup. Default=False.',
|
||||
default=False)
|
||||
@utils.arg('--force',
|
||||
action='store_true',
|
||||
help='Allows or disallows backup of a volume '
|
||||
'when the volume is attached to an instance. '
|
||||
'If set to True, backs up the volume whether '
|
||||
'its status is "available" or "in-use". The backup '
|
||||
'of an "in-use" volume means your data is crash '
|
||||
'consistent. Default=False.',
|
||||
default=False)
|
||||
@utils.service_type('volumev2')
|
||||
def do_backup_create(cs, args):
|
||||
"""Creates a volume backup."""
|
||||
@@ -1180,7 +1257,8 @@ def do_backup_create(cs, args):
|
||||
args.container,
|
||||
args.name,
|
||||
args.description,
|
||||
args.incremental)
|
||||
args.incremental,
|
||||
args.force)
|
||||
|
||||
info = {"volume_id": volume.id}
|
||||
info.update(backup._info)
|
||||
@@ -1203,10 +1281,45 @@ def do_backup_show(cs, args):
|
||||
utils.print_dict(info)
|
||||
|
||||
|
||||
@utils.arg('--all-tenants',
|
||||
metavar='<all_tenants>',
|
||||
nargs='?',
|
||||
type=int,
|
||||
const=1,
|
||||
default=0,
|
||||
help='Shows details for all tenants. Admin only.')
|
||||
@utils.arg('--all_tenants',
|
||||
nargs='?',
|
||||
type=int,
|
||||
const=1,
|
||||
help=argparse.SUPPRESS)
|
||||
@utils.arg('--name',
|
||||
metavar='<name>',
|
||||
default=None,
|
||||
help='Filters results by a name. Default=None.')
|
||||
@utils.arg('--status',
|
||||
metavar='<status>',
|
||||
default=None,
|
||||
help='Filters results by a status. Default=None.')
|
||||
@utils.arg('--volume-id',
|
||||
metavar='<volume-id>',
|
||||
default=None,
|
||||
help='Filters results by a volume ID. Default=None.')
|
||||
@utils.arg('--volume_id',
|
||||
help=argparse.SUPPRESS)
|
||||
@utils.service_type('volumev2')
|
||||
def do_backup_list(cs, args):
|
||||
"""Lists all backups."""
|
||||
backups = cs.backups.list()
|
||||
|
||||
search_opts = {
|
||||
'all_tenants': args.all_tenants,
|
||||
'name': args.name,
|
||||
'status': args.status,
|
||||
'volume_id': args.volume_id,
|
||||
}
|
||||
|
||||
backups = cs.backups.list(search_opts=search_opts)
|
||||
_translate_volume_snapshot_keys(backups)
|
||||
columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count',
|
||||
'Container']
|
||||
utils.print_list(backups, columns)
|
||||
@@ -1238,7 +1351,15 @@ def do_backup_restore(cs, args):
|
||||
volume_id = utils.find_volume(cs, vol).id
|
||||
else:
|
||||
volume_id = None
|
||||
cs.restores.restore(args.backup, volume_id)
|
||||
|
||||
restore = cs.restores.restore(args.backup, volume_id)
|
||||
|
||||
info = {"backup_id": args.backup}
|
||||
info.update(restore._info)
|
||||
|
||||
info.pop('links', None)
|
||||
|
||||
utils.print_dict(info)
|
||||
|
||||
|
||||
@utils.arg('backup', metavar='<backup>',
|
||||
@@ -1934,7 +2055,9 @@ def do_unmanage(cs, args):
|
||||
|
||||
|
||||
@utils.arg('volume', metavar='<volume>',
|
||||
help='Name or ID of the volume to promote.')
|
||||
help='Name or ID of the volume to promote. '
|
||||
'The volume should have the replica volume created with '
|
||||
'source-replica argument.')
|
||||
@utils.service_type('volumev2')
|
||||
def do_replication_promote(cs, args):
|
||||
"""Promote a secondary volume to primary for a relationship."""
|
||||
@@ -1943,7 +2066,8 @@ def do_replication_promote(cs, args):
|
||||
|
||||
|
||||
@utils.arg('volume', metavar='<volume>',
|
||||
help='Name or ID of the volume to reenable replication.')
|
||||
help='Name or ID of the volume to reenable replication. '
|
||||
'The replication-status of the volume should be inactive.')
|
||||
@utils.service_type('volumev2')
|
||||
def do_replication_reenable(cs, args):
|
||||
"""Sync the secondary volume with primary for a relationship."""
|
||||
@@ -2017,6 +2141,9 @@ def do_consisgroup_create(cs, args):
|
||||
@utils.arg('--cgsnapshot',
|
||||
metavar='<cgsnapshot>',
|
||||
help='Name or ID of a cgsnapshot. Default=None.')
|
||||
@utils.arg('--source-cg',
|
||||
metavar='<source-cg>',
|
||||
help='Name or ID of a source CG. Default=None.')
|
||||
@utils.arg('--name',
|
||||
metavar='<name>',
|
||||
help='Name of a consistency group. Default=None.')
|
||||
@@ -2025,14 +2152,24 @@ def do_consisgroup_create(cs, args):
|
||||
help='Description of a consistency group. Default=None.')
|
||||
@utils.service_type('volumev2')
|
||||
def do_consisgroup_create_from_src(cs, args):
|
||||
"""Creates a consistency group from a cgsnapshot."""
|
||||
if not args.cgsnapshot:
|
||||
msg = ('Cannot create consistency group because the source '
|
||||
'cgsnapshot is not provided.')
|
||||
"""Creates a consistency group from a cgsnapshot or a source CG."""
|
||||
if not args.cgsnapshot and not args.source_cg:
|
||||
msg = ('Cannot create consistency group because neither '
|
||||
'cgsnapshot nor source CG is provided.')
|
||||
raise exceptions.BadRequest(code=400, message=msg)
|
||||
cgsnapshot = _find_cgsnapshot(cs, args.cgsnapshot)
|
||||
if args.cgsnapshot and args.source_cg:
|
||||
msg = ('Cannot create consistency group because both '
|
||||
'cgsnapshot and source CG are provided.')
|
||||
raise exceptions.BadRequest(code=400, message=msg)
|
||||
cgsnapshot = None
|
||||
if args.cgsnapshot:
|
||||
cgsnapshot = _find_cgsnapshot(cs, args.cgsnapshot)
|
||||
source_cg = None
|
||||
if args.source_cg:
|
||||
source_cg = _find_consistencygroup(cs, args.source_cg)
|
||||
info = cs.consistencygroups.create_from_src(
|
||||
cgsnapshot.id,
|
||||
cgsnapshot.id if cgsnapshot else None,
|
||||
source_cg.id if source_cg else None,
|
||||
args.name,
|
||||
args.description)
|
||||
|
||||
@@ -2221,3 +2358,20 @@ def do_get_pools(cs, args):
|
||||
if args.detail:
|
||||
backend.update(info['capabilities'])
|
||||
utils.print_dict(backend)
|
||||
|
||||
|
||||
@utils.arg('host',
|
||||
metavar='<host>',
|
||||
help='Cinder host to show backend volume stats and properties; '
|
||||
'takes the form: host@backend-name')
|
||||
@utils.service_type('volumev2')
|
||||
def do_get_capabilities(cs, args):
|
||||
"""Show backend volume stats and properties. Admin only."""
|
||||
|
||||
capabilities = cs.capabilities.get(args.host)
|
||||
infos = dict()
|
||||
infos.update(capabilities._info)
|
||||
|
||||
prop = infos.pop('properties', None)
|
||||
utils.print_dict(infos, "Volume stats")
|
||||
utils.print_dict(prop, "Backend properties")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"""
|
||||
Volume Backups interface (1.1 extension).
|
||||
"""
|
||||
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from cinderclient import base
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class VolumeBackupManager(base.ManagerWithFind):
|
||||
|
||||
def create(self, volume_id, container=None,
|
||||
name=None, description=None,
|
||||
incremental=False):
|
||||
incremental=False, force=False):
|
||||
"""Creates a volume backup.
|
||||
|
||||
:param volume_id: The ID of the volume to backup.
|
||||
@@ -46,13 +46,15 @@ class VolumeBackupManager(base.ManagerWithFind):
|
||||
:param name: The name of the backup.
|
||||
:param description: The description of the backup.
|
||||
:param incremental: Incremental backup.
|
||||
:param force: If True, allows an in-use volume to be backed up.
|
||||
:rtype: :class:`VolumeBackup`
|
||||
"""
|
||||
body = {'backup': {'volume_id': volume_id,
|
||||
'container': container,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'incremental': incremental}}
|
||||
'incremental': incremental,
|
||||
'force': force, }}
|
||||
return self._create('/backups', body, 'backup')
|
||||
|
||||
def get(self, backup_id):
|
||||
@@ -68,10 +70,16 @@ class VolumeBackupManager(base.ManagerWithFind):
|
||||
|
||||
:rtype: list of :class:`VolumeBackup`
|
||||
"""
|
||||
if detailed is True:
|
||||
return self._list("/backups/detail", "backups")
|
||||
else:
|
||||
return self._list("/backups", "backups")
|
||||
search_opts = search_opts or {}
|
||||
|
||||
qparams = dict((key, val) for key, val in search_opts.items() if val)
|
||||
|
||||
query_string = ("?%s" % urlencode(qparams)) if qparams else ""
|
||||
|
||||
detail = '/detail' if detailed else ''
|
||||
|
||||
return self._list("/backups%s%s" % (detail, query_string),
|
||||
"backups")
|
||||
|
||||
def delete(self, backup):
|
||||
"""Delete a volume backup.
|
||||
|
||||
@@ -98,6 +98,22 @@ class Volume(base.Resource):
|
||||
"""
|
||||
return self.manager.set_metadata(self, metadata)
|
||||
|
||||
def set_image_metadata(self, volume, metadata):
|
||||
"""Set a volume's image metadata.
|
||||
|
||||
:param volume : The :class: `Volume` to set metadata on
|
||||
:param metadata: A dict of key/value pairs to set
|
||||
"""
|
||||
return self.manager.set_image_metadata(self, volume, metadata)
|
||||
|
||||
def delete_image_metadata(self, volume, keys):
|
||||
"""Delete specified keys from volume's image metadata.
|
||||
|
||||
:param volume: The :class:`Volume`.
|
||||
:param keys: A list of keys to be removed.
|
||||
"""
|
||||
return self.manager.delete_image_metadata(self, volume, keys)
|
||||
|
||||
def upload_to_image(self, force, image_name, container_format,
|
||||
disk_format):
|
||||
"""Upload a volume to image service as an image."""
|
||||
@@ -111,9 +127,16 @@ class Volume(base.Resource):
|
||||
"""
|
||||
self.manager.force_delete(self)
|
||||
|
||||
def reset_state(self, state):
|
||||
"""Update the volume with the provided state."""
|
||||
self.manager.reset_state(self, state)
|
||||
def reset_state(self, state, attach_status=None, migration_status=None):
|
||||
"""Update the volume with the provided state.
|
||||
|
||||
:param state: The state of the volume to set.
|
||||
:param attach_status: The attach_status of the volume to be set,
|
||||
or None to keep the current status.
|
||||
:param migration_status: The migration_status of the volume to be set,
|
||||
or None to keep the current status.
|
||||
"""
|
||||
self.manager.reset_state(self, state, attach_status, migration_status)
|
||||
|
||||
def extend(self, volume, new_size):
|
||||
"""Extend the size of the specified volume.
|
||||
@@ -123,9 +146,9 @@ class Volume(base.Resource):
|
||||
"""
|
||||
self.manager.extend(self, new_size)
|
||||
|
||||
def migrate_volume(self, host, force_host_copy):
|
||||
def migrate_volume(self, host, force_host_copy, lock_volume):
|
||||
"""Migrate the volume to a new host."""
|
||||
self.manager.migrate_volume(self, host, force_host_copy)
|
||||
self.manager.migrate_volume(self, host, force_host_copy, lock_volume)
|
||||
|
||||
def retype(self, volume_type, policy):
|
||||
"""Change a volume's type."""
|
||||
@@ -475,6 +498,26 @@ class VolumeManager(base.ManagerWithFind):
|
||||
for k in keys:
|
||||
self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k))
|
||||
|
||||
def set_image_metadata(self, volume, metadata):
|
||||
"""Set a volume's image metadata.
|
||||
|
||||
:param volume: The :class:`Volume`.
|
||||
:param metadata: keys and the values to be set with.
|
||||
:type metadata: dict
|
||||
"""
|
||||
return self._action("os-set_image_metadata", volume,
|
||||
{'metadata': metadata})
|
||||
|
||||
def delete_image_metadata(self, volume, keys):
|
||||
"""Delete specified keys from volume's image metadata.
|
||||
|
||||
:param volume: The :class:`Volume`.
|
||||
:param keys: A list of keys to be removed.
|
||||
"""
|
||||
for key in keys:
|
||||
self._action("os-unset_image_metadata", volume,
|
||||
{'key': key})
|
||||
|
||||
def upload_to_image(self, volume, force, image_name, container_format,
|
||||
disk_format):
|
||||
"""Upload volume to image service as image.
|
||||
@@ -495,9 +538,23 @@ class VolumeManager(base.ManagerWithFind):
|
||||
"""
|
||||
return self._action('os-force_delete', base.getid(volume))
|
||||
|
||||
def reset_state(self, volume, state):
|
||||
"""Update the provided volume with the provided state."""
|
||||
return self._action('os-reset_status', volume, {'status': state})
|
||||
def reset_state(self, volume, state, attach_status=None,
|
||||
migration_status=None):
|
||||
"""Update the provided volume with the provided state.
|
||||
|
||||
:param volume: The :class:`Volume` to set the state.
|
||||
:param state: The state of the volume to be set.
|
||||
:param attach_status: The attach_status of the volume to be set,
|
||||
or None to keep the current status.
|
||||
:param migration_status: The migration_status of the volume to be set,
|
||||
or None to keep the current status.
|
||||
"""
|
||||
body = {'status': state}
|
||||
if attach_status:
|
||||
body.update({'attach_status': attach_status})
|
||||
if migration_status:
|
||||
body.update({'migration_status': migration_status})
|
||||
return self._action('os-reset_status', volume, body)
|
||||
|
||||
def extend(self, volume, new_size):
|
||||
"""Extend the size of the specified volume.
|
||||
@@ -518,16 +575,19 @@ class VolumeManager(base.ManagerWithFind):
|
||||
"""
|
||||
return self._get("/volumes/%s/encryption" % volume_id)._info
|
||||
|
||||
def migrate_volume(self, volume, host, force_host_copy):
|
||||
def migrate_volume(self, volume, host, force_host_copy, lock_volume):
|
||||
"""Migrate volume to new host.
|
||||
|
||||
:param volume: The :class:`Volume` to migrate
|
||||
:param host: The destination host
|
||||
:param force_host_copy: Skip driver optimizations
|
||||
:param lock_volume: Lock the volume and guarantee the migration
|
||||
to finish
|
||||
"""
|
||||
return self._action('os-migrate_volume',
|
||||
volume,
|
||||
{'host': host, 'force_host_copy': force_host_copy})
|
||||
{'host': host, 'force_host_copy': force_host_copy,
|
||||
'lock_volume': lock_volume})
|
||||
|
||||
def migrate_volume_completion(self, old_volume, new_volume, error):
|
||||
"""Complete the migration from the old volume to the temp new one.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr<2.0,>=0.11
|
||||
pbr<2.0,>=1.6
|
||||
argparse
|
||||
PrettyTable<0.8,>=0.7
|
||||
python-keystoneclient>=1.6.0
|
||||
|
||||
2
setup.py
2
setup.py
@@ -25,5 +25,5 @@ except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
setup_requires=['pbr>=1.3'],
|
||||
pbr=True)
|
||||
|
||||
@@ -6,7 +6,7 @@ hacking<0.11,>=0.10.0
|
||||
coverage>=3.6
|
||||
discover
|
||||
fixtures>=1.3.1
|
||||
mock>=1.0
|
||||
mock>=1.2
|
||||
oslosphinx>=2.5.0 # Apache-2.0
|
||||
python-subunit>=0.0.18
|
||||
requests-mock>=0.6.0 # Apache-2.0
|
||||
|
||||
Reference in New Issue
Block a user