summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.rst4
-rw-r--r--HACKING.rst4
-rw-r--r--README.rst14
-rw-r--r--cinderclient/_i18n.py2
-rw-r--r--cinderclient/api_versions.py11
-rw-r--r--cinderclient/base.py13
-rw-r--r--cinderclient/client.py19
-rw-r--r--cinderclient/exceptions.py23
-rw-r--r--cinderclient/shell_utils.py41
-rw-r--r--cinderclient/tests/functional/test_cli.py3
-rw-r--r--cinderclient/tests/unit/test_api_versions.py9
-rw-r--r--cinderclient/tests/unit/test_base.py31
-rw-r--r--cinderclient/tests/unit/test_client.py34
-rw-r--r--cinderclient/tests/unit/test_utils.py2
-rw-r--r--cinderclient/tests/unit/v2/fakes.py8
-rw-r--r--cinderclient/tests/unit/v2/test_quotas.py5
-rw-r--r--cinderclient/tests/unit/v3/fakes.py53
-rw-r--r--cinderclient/tests/unit/v3/test_groups.py54
-rw-r--r--cinderclient/tests/unit/v3/test_services.py42
-rw-r--r--cinderclient/tests/unit/v3/test_shell.py258
-rw-r--r--cinderclient/tests/unit/v3/test_volumes.py32
-rw-r--r--cinderclient/v2/shell.py25
-rw-r--r--cinderclient/v2/volumes.py25
-rw-r--r--cinderclient/v3/groups.py71
-rw-r--r--cinderclient/v3/services.py29
-rw-r--r--cinderclient/v3/shell.py179
-rw-r--r--cinderclient/v3/volumes.py25
-rw-r--r--doc/source/conf.py14
-rw-r--r--doc/source/functional_tests.rst2
-rw-r--r--doc/source/index.rst2
-rw-r--r--doc/source/unit_tests.rst4
-rw-r--r--releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml10
-rw-r--r--releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml4
-rw-r--r--releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml3
-rw-r--r--releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml6
-rw-r--r--releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml3
-rw-r--r--releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml6
-rw-r--r--releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml4
-rw-r--r--releasenotes/source/conf.py10
-rw-r--r--requirements.txt2
-rw-r--r--test-requirements.txt8
41 files changed, 1010 insertions, 84 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 87335db..80ea3b5 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,13 +1,13 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:
- http://docs.openstack.org/infra/manual/developers.html
+ https://docs.openstack.org/infra/manual/developers.html
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
- http://docs.openstack.org/infra/manual/developers.html#development-workflow
+ https://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
diff --git a/HACKING.rst b/HACKING.rst
index 03844f1..fed3611 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -2,7 +2,7 @@ Cinder Client Style Commandments
================================
- Step 1: Read the OpenStack Style Commandments
- http://docs.openstack.org/developer/hacking/
+ https://docs.openstack.org/hacking/latest/
- Step 2: Read on
Cinder Client Specific Commandments
@@ -29,7 +29,7 @@ Release Notes
- Cinder Client uses Reno for release notes management. See the `Reno Documentation`_
for more details on its usage.
-.. _Reno Documentation: http://docs.openstack.org/developer/reno/
+.. _Reno Documentation: https://docs.openstack.org/reno/latest/
- As a quick example, when adding a new shell command for Awesome Storage Feature, one
could perform the following steps to include a release note for the new feature:
diff --git a/README.rst b/README.rst
index c9fe419..17f4aac 100644
--- a/README.rst
+++ b/README.rst
@@ -26,15 +26,15 @@ See the `OpenStack CLI Reference`_ for information on how to use the ``cinder``
command-line tool. You may also want to look at the
`OpenStack API documentation`_.
-.. _OpenStack CLI Reference: http://docs.openstack.org/cli-reference/overview.html
-.. _OpenStack API documentation: http://developer.openstack.org/api-ref.html
+.. _OpenStack CLI Reference: https://docs.openstack.org/python-openstackclient/latest/cli/
+.. _OpenStack API documentation: https://developer.openstack.org/api-guide/quick-start/
The project is hosted on `Launchpad`_, where bugs can be filed. The code is
hosted on `OpenStack`_. Patches must be submitted using `Gerrit`_.
.. _OpenStack: https://git.openstack.org/cgit/openstack/python-cinderclient
.. _Launchpad: https://launchpad.net/python-cinderclient
-.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
+.. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow
This code is a fork of `Jacobian's python-cloudservers`__. If you need API support
for the Rackspace API solely or the BSD license, you should use that repository.
@@ -52,12 +52,12 @@ __ https://github.com/jacobian-archive/python-cloudservers
* `How to Contribute`_
.. _PyPi: https://pypi.python.org/pypi/python-cinderclient
-.. _Online Documentation: http://docs.openstack.org/developer/python-cinderclient
+.. _Online Documentation: https://docs.openstack.org/python-cinderclient/latest/
.. _Blueprints: https://blueprints.launchpad.net/python-cinderclient
.. _Bugs: https://bugs.launchpad.net/python-cinderclient
.. _Source: https://git.openstack.org/cgit/openstack/python-cinderclient
-.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html
-.. _Specs: http://specs.openstack.org/openstack/cinder-specs/
+.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html
+.. _Specs: https://specs.openstack.org/openstack/cinder-specs/
.. contents:: Contents:
@@ -366,4 +366,4 @@ Quick-start using keystone::
>>> nt.volumes.list()
[...]
-See release notes and more at `<http://docs.openstack.org/developer/python-cinderclient/>`_.
+See release notes and more at `<https://docs.openstack.org/python-cinderclient/latest/>`_.
diff --git a/cinderclient/_i18n.py b/cinderclient/_i18n.py
index 1653da7..96c9246 100644
--- a/cinderclient/_i18n.py
+++ b/cinderclient/_i18n.py
@@ -14,7 +14,7 @@
"""oslo.i18n integration module.
-See http://docs.openstack.org/developer/oslo.i18n/usage.html .
+See https://docs.openstack.org/oslo.i18n/latest/user/usage.html .
"""
diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py
index d1bb1e1..537b779 100644
--- a/cinderclient/api_versions.py
+++ b/cinderclient/api_versions.py
@@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__)
# key is a deprecated version and value is an alternative version.
DEPRECATED_VERSIONS = {"1": "2"}
DEPRECATED_VERSION = "2.0"
-MAX_VERSION = "3.33"
+MAX_VERSION = "3.40"
MIN_VERSION = "3.0"
_SUBSTITUTIONS = {}
@@ -235,7 +235,14 @@ def get_api_version(version_string):
def _get_server_version_range(client):
- versions = client.services.server_api_version()
+ try:
+ versions = client.services.server_api_version()
+ except AttributeError:
+ # Wrong client was used, translate to something helpful.
+ raise exceptions.UnsupportedVersion(
+ _('Invalid client version %s to get server version range. Only '
+ 'the v3 client is supported for this operation.') %
+ client.version)
if not versions:
return APIVersion(), APIVersion()
diff --git a/cinderclient/base.py b/cinderclient/base.py
index 88ab951..83f8731 100644
--- a/cinderclient/base.py
+++ b/cinderclient/base.py
@@ -115,12 +115,13 @@ class Manager(common_base.HookableMixin):
# than osapi_max_limit, so we have to retrieve multiple times to
# get the complete list.
next = None
- if 'volumes_links' in body:
- volumes_links = body['volumes_links']
- if volumes_links:
- for volumes_link in volumes_links:
- if 'rel' in volumes_link and 'next' == volumes_link['rel']:
- next = volumes_link['href']
+ link_name = response_key + '_links'
+ if link_name in body:
+ links = body[link_name]
+ if links:
+ for link in links:
+ if 'rel' in link and 'next' == link['rel']:
+ next = link['href']
break
if next:
# As long as the 'next' link is not empty, keep requesting it
diff --git a/cinderclient/client.py b/cinderclient/client.py
index 485be9f..dabfcd3 100644
--- a/cinderclient/client.py
+++ b/cinderclient/client.py
@@ -78,6 +78,8 @@ def get_server_version(url):
:returns: APIVersion object for min and max version supported by
the server
"""
+ min_version = "2.0"
+ current_version = "2.0"
logger = logging.getLogger(__name__)
try:
@@ -87,19 +89,21 @@ def get_server_version(url):
versions = data['versions']
for version in versions:
if '3.' in version['version']:
- return (api_versions.APIVersion(version['min_version']),
- api_versions.APIVersion(version['version']))
+ min_version = version['min_version']
+ current_version = version['version']
+ break
except exceptions.ClientException as e:
logger.warning("Error in server version query:%s\n"
"Returning APIVersion 2.0", six.text_type(e.message))
- return api_versions.APIVersion("2.0"), api_versions.APIVersion("2.0")
+ return (api_versions.APIVersion(min_version),
+ api_versions.APIVersion(current_version))
def get_highest_client_server_version(url):
+ """Returns highest supported version by client and server as a string."""
min_server, max_server = get_server_version(url)
- max_server_version = api_versions.APIVersion.get_string(max_server)
-
- return min(float(max_server_version), float(api_versions.MAX_VERSION))
+ max_client = api_versions.APIVersion(api_versions.MAX_VERSION)
+ return min(max_server, max_client).get_string()
def get_volume_api_from_url(url):
@@ -149,6 +153,9 @@ class SessionClient(adapter.LegacyJsonAdapter):
if raise_exc and resp.status_code >= 400:
raise exceptions.from_response(resp, body)
+ if not self.global_request_id:
+ self.global_request_id = resp.headers.get('x-openstack-request-id')
+
return resp, body
def _cs_request(self, url, method, **kwargs):
diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py
index 43ae5a4..4f0227e 100644
--- a/cinderclient/exceptions.py
+++ b/cinderclient/exceptions.py
@@ -21,6 +21,29 @@ from datetime import datetime
from oslo_utils import timeutils
+class ResourceInErrorState(Exception):
+ """When resource is in Error state"""
+ def __init__(self, obj, fault_msg):
+ msg = "'%s' resource is in the error state" % obj.__class__.__name__
+ if fault_msg:
+ msg += " due to '%s'" % fault_msg
+ self.message = "%s." % msg
+
+ def __str__(self):
+ return self.message
+
+
+class TimeoutException(Exception):
+ """When an action exceeds the timeout period to complete the action"""
+ def __init__(self, obj, action):
+ self.message = ("The '%(action)s' of the '%(object_name)s' exceeded "
+ "the timeout period." % {"action": action,
+ "object_name": obj.__class__.__name__})
+
+ def __str__(self):
+ return self.message
+
+
class UnsupportedVersion(Exception):
"""Indicates that the user is trying to use an unsupported
version of the API.
diff --git a/cinderclient/shell_utils.py b/cinderclient/shell_utils.py
index 4cb676b..e01b0cf 100644
--- a/cinderclient/shell_utils.py
+++ b/cinderclient/shell_utils.py
@@ -18,11 +18,11 @@ import sys
import time
from cinderclient import utils
+from cinderclient import exceptions
_quota_resources = ['volumes', 'snapshots', 'gigabytes',
'backups', 'backup_gigabytes',
- 'consistencygroups', 'per_volume_gigabytes',
- 'groups', ]
+ 'per_volume_gigabytes', 'groups', ]
_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit']
@@ -151,7 +151,9 @@ def extract_filters(args):
(key, value) = f.split('=', 1)
if value.startswith('{') and value.endswith('}'):
value = _build_internal_dict(value[1:-1])
- filters[key] = value
+ filters[key] = value
+ else:
+ print("WARNING: Ignoring the filter %s while showing result." % f)
return filters
@@ -275,3 +277,36 @@ def print_qos_specs_and_associations_list(q_specs):
def print_associations_list(associations):
utils.print_list(associations, ['Association_Type', 'Name', 'ID'])
+
+
+def _poll_for_status(poll_fn, obj_id, info, action, final_ok_states,
+ timeout_period, global_request_id=None, messages=None,
+ poll_period=2, status_field="status"):
+ """Block while an action is being performed."""
+ time_elapsed = 0
+ while True:
+ time.sleep(poll_period)
+ time_elapsed += poll_period
+ obj = poll_fn(obj_id)
+ status = getattr(obj, status_field)
+ info[status_field] = status
+ if status:
+ status = status.lower()
+
+ if status in final_ok_states:
+ break
+ elif status == "error":
+ utils.print_dict(info)
+ if global_request_id:
+ search_opts = {
+ 'request_id': global_request_id
+ }
+ message_list = messages.list(search_opts=search_opts)
+ try:
+ fault_msg = message_list[0].user_message
+ except IndexError:
+ fault_msg = "Unknown error. Operation failed."
+ raise exceptions.ResourceInErrorState(obj, fault_msg)
+ elif time_elapsed == timeout_period:
+ utils.print_dict(info)
+ raise exceptions.TimeoutException(obj, action)
diff --git a/cinderclient/tests/functional/test_cli.py b/cinderclient/tests/functional/test_cli.py
index 01083c4..8047459 100644
--- a/cinderclient/tests/functional/test_cli.py
+++ b/cinderclient/tests/functional/test_cli.py
@@ -17,7 +17,8 @@ from cinderclient.tests.functional import base
class CinderVolumeTests(base.ClientTestBase):
"""Check of base cinder volume commands."""
- VOLUME_PROPERTY = ('attachments', 'availability_zone', 'bootable',
+ VOLUME_PROPERTY = ('attachment_ids', 'attached_servers',
+ 'availability_zone', 'bootable',
'created_at', 'description', 'encrypted', 'id',
'metadata', 'name', 'size', 'status',
'user_id', 'volume_type')
diff --git a/cinderclient/tests/unit/test_api_versions.py b/cinderclient/tests/unit/test_api_versions.py
index ef8f311..5f14c3e 100644
--- a/cinderclient/tests/unit/test_api_versions.py
+++ b/cinderclient/tests/unit/test_api_versions.py
@@ -15,8 +15,10 @@
import ddt
import mock
+import six
from cinderclient import api_versions
+from cinderclient import client as base_client
from cinderclient import exceptions
from cinderclient.v3 import client
from cinderclient.tests.unit import utils
@@ -259,3 +261,10 @@ class DiscoverVersionTestCase(utils.TestCase):
highest_version = api_versions.get_highest_version(self.fake_client)
self.assertEqual("3.14", highest_version.get_string())
self.assertTrue(self.fake_client.services.server_api_version.called)
+
+ def test_get_highest_version_bad_client(self):
+ """Tests that we gracefully handle the wrong version of client."""
+ v2_client = base_client.Client('2.0')
+ ex = self.assertRaises(exceptions.UnsupportedVersion,
+ api_versions.get_highest_version, v2_client)
+ self.assertIn('Invalid client version 2.0 to get', six.text_type(ex))
diff --git a/cinderclient/tests/unit/test_base.py b/cinderclient/tests/unit/test_base.py
index ce8d9e4..d4dd517 100644
--- a/cinderclient/tests/unit/test_base.py
+++ b/cinderclient/tests/unit/test_base.py
@@ -126,6 +126,37 @@ class BaseTest(utils.TestCase):
manager._build_list_url,
**arguments)
+ def test__list_no_link(self):
+ api = mock.Mock()
+ api.client.get.return_value = (mock.sentinel.resp,
+ {'resp_keys': [{'name': '1'}]})
+ manager = test_utils.FakeManager(api)
+ res = manager._list(mock.sentinel.url, 'resp_keys')
+ api.client.get.assert_called_once_with(mock.sentinel.url)
+ result = [r.name for r in res]
+ self.assertListEqual(['1'], result)
+
+ def test__list_with_link(self):
+ api = mock.Mock()
+ api.client.get.side_effect = [
+ (mock.sentinel.resp,
+ {'resp_keys': [{'name': '1'}],
+ 'resp_keys_links': [{'rel': 'next', 'href': mock.sentinel.u2}]}),
+ (mock.sentinel.resp,
+ {'resp_keys': [{'name': '2'}],
+ 'resp_keys_links': [{'rel': 'next', 'href': mock.sentinel.u3}]}),
+ (mock.sentinel.resp,
+ {'resp_keys': [{'name': '3'}],
+ 'resp_keys_links': [{'rel': 'next', 'href': None}]}),
+ ]
+ manager = test_utils.FakeManager(api)
+ res = manager._list(mock.sentinel.url, 'resp_keys')
+ api.client.get.assert_has_calls([mock.call(mock.sentinel.url),
+ mock.call(mock.sentinel.u2),
+ mock.call(mock.sentinel.u3)])
+ result = [r.name for r in res]
+ self.assertListEqual(['1', '2', '3'], result)
+
class ListWithMetaTest(utils.TestCase):
def test_list_with_meta(self):
diff --git a/cinderclient/tests/unit/test_client.py b/cinderclient/tests/unit/test_client.py
index 8048180..bd3b3f6 100644
--- a/cinderclient/tests/unit/test_client.py
+++ b/cinderclient/tests/unit/test_client.py
@@ -14,6 +14,7 @@
import json
import logging
+import ddt
import fixtures
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as keystone_exception
@@ -130,9 +131,11 @@ class ClientTest(utils.TestCase):
"code": 202
}
+ request_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e"
mock_response = utils.TestResponse({
"status_code": 202,
"text": six.b(json.dumps(resp)),
+ "headers": {"x-openstack-request-id": request_id},
})
# 'request' method of Adaptor will return 202 response
@@ -309,9 +312,26 @@ class ClientTestSensitiveInfo(utils.TestCase):
self.assertNotIn(auth_password, output[1], output)
+@ddt.ddt
class GetAPIVersionTestCase(utils.TestCase):
@mock.patch('cinderclient.client.requests.get')
+ def test_get_server_version_v2(self, mock_request):
+
+ mock_response = utils.TestResponse({
+ "status_code": 200,
+ "text": json.dumps(fakes.fake_request_get_no_v3())
+ })
+
+ mock_request.return_value = mock_response
+
+ url = "http://192.168.122.127:8776/v2/e5526285ebd741b1819393f772f11fc3"
+
+ min_version, max_version = cinderclient.client.get_server_version(url)
+ self.assertEqual(api_versions.APIVersion('2.0'), min_version)
+ self.assertEqual(api_versions.APIVersion('2.0'), max_version)
+
+ @mock.patch('cinderclient.client.requests.get')
def test_get_server_version(self, mock_request):
mock_response = utils.TestResponse({
@@ -334,7 +354,8 @@ class GetAPIVersionTestCase(utils.TestCase):
self.assertEqual(max_version, api_versions.APIVersion('3.16'))
@mock.patch('cinderclient.client.requests.get')
- def test_get_highest_client_server_version(self, mock_request):
+ @ddt.data('3.12', '3.40')
+ def test_get_highest_client_server_version(self, version, mock_request):
mock_response = utils.TestResponse({
"status_code": 200,
@@ -345,9 +366,8 @@ class GetAPIVersionTestCase(utils.TestCase):
url = "http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3"
- highest = cinderclient.client.get_highest_client_server_version(url)
- current_client_MAX_VERSION = float(api_versions.MAX_VERSION)
- if current_client_MAX_VERSION > 3.16:
- self.assertEqual(3.16, highest)
- else:
- self.assertEqual(current_client_MAX_VERSION, highest)
+ with mock.patch.object(api_versions, 'MAX_VERSION', version):
+ highest = (
+ cinderclient.client.get_highest_client_server_version(url))
+ expected = version if version == '3.12' else '3.16'
+ self.assertEqual(expected, highest)
diff --git a/cinderclient/tests/unit/test_utils.py b/cinderclient/tests/unit/test_utils.py
index a2d2256..7f39f06 100644
--- a/cinderclient/tests/unit/test_utils.py
+++ b/cinderclient/tests/unit/test_utils.py
@@ -34,7 +34,7 @@ UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
class FakeResource(object):
NAME_ATTR = 'name'
- def __init__(self, _id, properties):
+ def __init__(self, _id, properties, **kwargs):
self.id = _id
try:
self.name = properties['name']
diff --git a/cinderclient/tests/unit/v2/fakes.py b/cinderclient/tests/unit/v2/fakes.py
index a21b7b2..a4c5d39 100644
--- a/cinderclient/tests/unit/v2/fakes.py
+++ b/cinderclient/tests/unit/v2/fakes.py
@@ -28,7 +28,8 @@ REQUEST_ID = 'req-test-request-id'
def _stub_volume(*args, **kwargs):
volume = {
"migration_status": None,
- "attachments": [{u'server_id': u'1234'}],
+ "attachments": [{u'server_id': u'1234', u'id':
+ u'3f88836f-adde-4296-9f6b-2c59a0bcda9a'}],
"links": [
{
"href": "http://localhost/v2/fake/volumes/1234",
@@ -535,6 +536,8 @@ class FakeHTTPClient(base_client.HTTPClient):
elif action == 'os-volume_upload_image':
assert 'image_name' in body[action]
_body = body
+ elif action == 'revert':
+ assert 'snapshot_id' in body[action]
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
@@ -625,7 +628,6 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
- 'consistencygroups': 1,
'per_volume_gigabytes': 1, }})
def get_os_quota_sets_test_defaults(self):
@@ -637,7 +639,6 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
- 'consistencygroups': 1,
'per_volume_gigabytes': 1, }})
def put_os_quota_sets_test(self, body, **kw):
@@ -652,7 +653,6 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
- 'consistencygroups': 2,
'per_volume_gigabytes': 1, }})
def delete_os_quota_sets_1234(self, **kw):
diff --git a/cinderclient/tests/unit/v2/test_quotas.py b/cinderclient/tests/unit/v2/test_quotas.py
index 6dd20b0..bb29e4d 100644
--- a/cinderclient/tests/unit/v2/test_quotas.py
+++ b/cinderclient/tests/unit/v2/test_quotas.py
@@ -41,7 +41,6 @@ class QuotaSetsTest(utils.TestCase):
q.update(gigabytes=2000)
q.update(backups=2)
q.update(backup_gigabytes=2000)
- q.update(consistencygroups=2)
q.update(per_volume_gigabytes=100)
cs.assert_called('PUT', '/os-quota-sets/test')
self._assert_request_id(q)
@@ -54,7 +53,6 @@ class QuotaSetsTest(utils.TestCase):
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
- self.assertEqual(q.consistencygroups, q2.consistencygroups)
self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes)
q2.volumes = 0
self.assertNotEqual(q.volumes, q2.volumes)
@@ -66,8 +64,6 @@ class QuotaSetsTest(utils.TestCase):
self.assertNotEqual(q.backups, q2.backups)
q2.backup_gigabytes = 0
self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes)
- q2.consistencygroups = 0
- self.assertNotEqual(q.consistencygroups, q2.consistencygroups)
q2.per_volume_gigabytes = 0
self.assertNotEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes)
q2.get()
@@ -76,7 +72,6 @@ class QuotaSetsTest(utils.TestCase):
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
- self.assertEqual(q.consistencygroups, q2.consistencygroups)
self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes)
self._assert_request_id(q)
self._assert_request_id(q2)
diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py
index 25a8151..04c5aa0 100644
--- a/cinderclient/tests/unit/v3/fakes.py
+++ b/cinderclient/tests/unit/v3/fakes.py
@@ -77,8 +77,9 @@ class FakeClient(fakes.FakeClient, client.Client):
'project_id', 'auth_url',
extensions=kwargs.get('extensions'))
self.api_version = api_version
+ global_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e"
self.client = FakeHTTPClient(api_version=api_version,
- **kwargs)
+ global_request_id=global_id, **kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()
@@ -369,6 +370,9 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
action = list(body)[0]
if action == 'delete':
assert 'delete-volumes' in body[action]
+ elif action in ('enable_replication', 'disable_replication',
+ 'failover_replication', 'list_replication_targets'):
+ assert action in body
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, {})
@@ -544,6 +548,25 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
}
return 200, {}, {'message': message}
+ def put_os_services_set_log(self, body):
+ return (202, {}, {})
+
+ def put_os_services_get_log(self, body):
+ levels = [{'binary': 'cinder-api', 'host': 'host1',
+ 'levels': {'prefix1': 'DEBUG', 'prefix2': 'INFO'}},
+ {'binary': 'cinder-volume', 'host': 'host@backend#pool',
+ 'levels': {'prefix3': 'WARNING', 'prefix4': 'ERROR'}}]
+ return (200, {}, {'log_levels': levels})
+
+ def get_volumes_summary(self, **kw):
+ return 200, {}, {"volume-summary": {'total_size': 5,
+ 'total_count': 5,
+ 'metadata': {
+ "test_key": ["test_value"]
+ }
+ }
+ }
+
#
# resource filters
#
@@ -589,3 +612,31 @@ def fake_request_get():
'updated': '2016-02-08T12:20:21Z',
'version': '3.16'}]}
return versions
+
+
+def fake_request_get_no_v3():
+ versions = {'versions': [{'id': 'v1.0',
+ 'links': [{'href': 'http://docs.openstack.org/',
+ 'rel': 'describedby',
+ 'type': 'text/html'},
+ {'href': 'http://192.168.122.197/v1/',
+ 'rel': 'self'}],
+ 'media-types': [{'base': 'application/json',
+ 'type': 'application/'}],
+ 'min_version': '',
+ 'status': 'DEPRECATED',
+ 'updated': '2016-05-02T20:25:19Z',
+ 'version': ''},
+ {'id': 'v2.0',
+ 'links': [{'href': 'http://docs.openstack.org/',
+ 'rel': 'describedby',
+ 'type': 'text/html'},
+ {'href': 'http://192.168.122.197/v2/',
+ 'rel': 'self'}],
+ 'media-types': [{'base': 'application/json',
+ 'type': 'application/'}],
+ 'min_version': '',
+ 'status': 'SUPPORTED',
+ 'updated': '2014-06-28T12:20:21Z',
+ 'version': ''}]}
+ return versions
diff --git a/cinderclient/tests/unit/v3/test_groups.py b/cinderclient/tests/unit/v3/test_groups.py
index 72e72a0..d74e2a0 100644
--- a/cinderclient/tests/unit/v3/test_groups.py
+++ b/cinderclient/tests/unit/v3/test_groups.py
@@ -158,3 +158,57 @@ class GroupsTest(utils.TestCase):
cs.assert_called('POST', '/groups/action',
body=expected)
self._assert_request_id(grp)
+
+ def test_enable_replication_group(self):
+ expected = {'enable_replication': {}}
+ g0 = cs.groups.list()[0]
+ grp = g0.enable_replication()
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.enable_replication('1234')
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.enable_replication(g0)
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+
+ def test_disable_replication_group(self):
+ expected = {'disable_replication': {}}
+ g0 = cs.groups.list()[0]
+ grp = g0.disable_replication()
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.disable_replication('1234')
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.disable_replication(g0)
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+
+ def test_failover_replication_group(self):
+ expected = {'failover_replication':
+ {'allow_attached_volume': False,
+ 'secondary_backend_id': None}}
+ g0 = cs.groups.list()[0]
+ grp = g0.failover_replication()
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.failover_replication('1234')
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.failover_replication(g0)
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+
+ def test_list_replication_targets(self):
+ expected = {'list_replication_targets': {}}
+ g0 = cs.groups.list()[0]
+ grp = g0.list_replication_targets()
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.list_replication_targets('1234')
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
+ grp = cs.groups.list_replication_targets(g0)
+ self._assert_request_id(grp)
+ cs.assert_called('POST', '/groups/1234/action', body=expected)
diff --git a/cinderclient/tests/unit/v3/test_services.py b/cinderclient/tests/unit/v3/test_services.py
index ac45298..92f7903 100644
--- a/cinderclient/tests/unit/v3/test_services.py
+++ b/cinderclient/tests/unit/v3/test_services.py
@@ -41,3 +41,45 @@ class ServicesTest(utils.TestCase):
client = fakes.FakeClient(version_header='3.0')
svs = client.services.server_api_version()
[self.assertIsInstance(s, services.Service) for s in svs]
+
+ def test_set_log_levels(self):
+ expected = {'level': 'debug', 'binary': 'cinder-api',
+ 'server': 'host1', 'prefix': 'sqlalchemy.'}
+
+ cs = fakes.FakeClient(version_header='3.32')
+ cs.services.set_log_levels(expected['level'], expected['binary'],
+ expected['server'], expected['prefix'])
+
+ cs.assert_called('PUT', '/os-services/set-log', body=expected)
+
+ def test_get_log_levels(self):
+ expected = {'binary': 'cinder-api', 'server': 'host1',
+ 'prefix': 'sqlalchemy.'}
+
+ cs = fakes.FakeClient(version_header='3.32')
+ result = cs.services.get_log_levels(expected['binary'],
+ expected['server'],
+ expected['prefix'])
+
+ cs.assert_called('PUT', '/os-services/get-log', body=expected)
+ expected = [services.LogLevel(cs.services,
+ {'binary': 'cinder-api', 'host': 'host1',
+ 'prefix': 'prefix1', 'level': 'DEBUG'},
+ loaded=True),
+ services.LogLevel(cs.services,
+ {'binary': 'cinder-api', 'host': 'host1',
+ 'prefix': 'prefix2', 'level': 'INFO'},
+ loaded=True),
+ services.LogLevel(cs.services,
+ {'binary': 'cinder-volume',
+ 'host': 'host@backend#pool',
+ 'prefix': 'prefix3',
+ 'level': 'WARNING'},
+ loaded=True),
+ services.LogLevel(cs.services,
+ {'binary': 'cinder-volume',
+ 'host': 'host@backend#pool',
+ 'prefix': 'prefix4', 'level': 'ERROR'},
+ loaded=True)]
+ # Since it will be sorted by the prefix we can compare them directly
+ self.assertListEqual(expected, result)
diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py
index 0b9431b..9221af8 100644
--- a/cinderclient/tests/unit/v3/test_shell.py
+++ b/cinderclient/tests/unit/v3/test_shell.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
@@ -13,15 +14,44 @@
# License for the specific language governing permissions and limitations
# under the License.
+
+# NOTE(geguileo): For v3 we cannot mock any of the following methods
+# - utils.find_volume
+# - shell_utils.find_backup
+# - shell_utils.find_volume_snapshot
+# - shell_utils.find_group
+# - shell_utils.find_group_snapshot
+# because we are caching them in cinderclient.v3.shell:RESET_STATE_RESOURCES
+# which means that our tests could fail depending on the mocking and loading
+# order.
+#
+# Alternatives are:
+# - Mock utils.find_resource when we have only 1 call to that method
+# - Use an auxiliary method that will call original method for irrelevant
+# calls. Example from test_revert_to_snapshot:
+# original = client_utils.find_resource
+#
+# def find_resource(manager, name_or_id, **kwargs):
+# if isinstance(manager, volume_snapshots.SnapshotManager):
+# return volume_snapshots.Snapshot(self,
+# {'id': '5678',
+# 'volume_id': '1234'})
+# return original(manager, name_or_id, **kwargs)
+
import ddt
import fixtures
import mock
from requests_mock.contrib import fixture as requests_mock_fixture
+import six
+import cinderclient
from cinderclient import client
from cinderclient import exceptions
from cinderclient import shell
+from cinderclient import utils as cinderclient_utils
+from cinderclient import base
from cinderclient.v3 import volumes
+from cinderclient.v3 import volume_snapshots
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v3 import fakes
from cinderclient.tests.unit.fixture_data import keystone_client
@@ -99,6 +129,10 @@ class ShellTest(utils.TestCase):
'list --filters name~=456',
'expected':
'/volumes/detail?name%7E=456'},
+ {'command':
+ u'list --filters name~=Σ',
+ 'expected':
+ '/volumes/detail?name%7E=%CE%A3'},
# testcases for list group
{'command':
'group-list --filters name=456',
@@ -197,6 +231,10 @@ class ShellTest(utils.TestCase):
# NOTE(jdg): we default to detail currently
self.assert_called('GET', '/volumes/detail')
+ def test_summary(self):
+ self.run_command('--os-volume-api-version 3.12 summary')
+ self.assert_called('GET', '/volumes/summary')
+
def test_list_with_group_id_before_3_10(self):
self.assertRaises(exceptions.UnsupportedAttribute,
self.run_command,
@@ -241,15 +279,36 @@ class ShellTest(utils.TestCase):
'mountpoint': '/123',
'initiator': 'aabbccdd',
'platform': 'x86_xx'},
+ 'volume_uuid': '1234'}},
+ {'cmd': 'abc 1233',
+ 'body': {'instance_uuid': '1233',
+ 'connector': {},
'volume_uuid': '1234'}})
+ @mock.patch('cinderclient.utils.find_resource')
@ddt.unpack
- def test_attachment_create(self, cmd, body):
+ def test_attachment_create(self, mock_find_volume, cmd, body):
+ mock_find_volume.return_value = volumes.Volume(self,
+ {'id': '1234'},
+ loaded=True)
command = '--os-volume-api-version 3.27 attachment-create '
command += cmd
self.run_command(command)
expected = {'attachment': body}
+ self.assertTrue(mock_find_volume.called)
self.assert_called('POST', '/attachments', body=expected)
+ @mock.patch.object(volumes.VolumeManager, 'findall')
+ def test_attachment_create_duplicate_name_vol(self, mock_findall):
+ found = [volumes.Volume(self, {'id': '7654', 'name': 'abc'},
+ loaded=True),
+ volumes.Volume(self, {'id': '9876', 'name': 'abc'},
+ loaded=True)]
+ mock_findall.return_value = found
+ self.assertRaises(exceptions.CommandError,
+ self.run_command,
+ '--os-volume-api-version 3.27 '
+ 'attachment-create abc 789')
+
@ddt.data({'cmd': '',
'expected': ''},
{'cmd': '--volume-id 1234',
@@ -273,6 +332,24 @@ class ShellTest(utils.TestCase):
self.run_command(command)
self.assert_called('GET', '/attachments%s' % expected)
+ def test_revert_to_snapshot(self):
+ original = cinderclient_utils.find_resource
+
+ def find_resource(manager, name_or_id, **kwargs):
+ if isinstance(manager, volume_snapshots.SnapshotManager):
+ return volume_snapshots.Snapshot(self,
+ {'id': '5678',
+ 'volume_id': '1234'})
+ return original(manager, name_or_id, **kwargs)
+
+ with mock.patch('cinderclient.utils.find_resource',
+ side_effect=find_resource):
+ self.run_command(
+ '--os-volume-api-version 3.40 revert-to-snapshot 5678')
+
+ self.assert_called('POST', '/volumes/1234/action',
+ body={'revert': {'snapshot_id': '5678'}})
+
def test_attachment_show(self):
self.run_command('--os-volume-api-version 3.27 attachment-show 1234')
self.assert_called('GET', '/attachments/1234')
@@ -712,7 +789,7 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('DELETE', '/messages/1234')
self.assert_called_anytime('DELETE', '/messages/12345')
- @mock.patch('cinderclient.utils.find_volume')
+ @mock.patch('cinderclient.utils.find_resource')
def test_delete_metadata(self, mock_find_volume):
mock_find_volume.return_value = volumes.Volume(self,
{'id': '1234',
@@ -736,3 +813,180 @@ class ShellTest(utils.TestCase):
command += ' --withreplication %s' % replication
self.run_command(command)
self.assert_called('GET', '/os-services')
+
+ def test_group_enable_replication(self):
+ cmd = '--os-volume-api-version 3.38 group-enable-replication 1234'
+ self.run_command(cmd)
+ expected = {'enable_replication': {}}
+ self.assert_called('POST', '/groups/1234/action', body=expected)
+
+ def test_group_disable_replication(self):
+ cmd = '--os-volume-api-version 3.38 group-disable-replication 1234'
+ self.run_command(cmd)
+ expected = {'disable_replication': {}}
+ self.assert_called('POST', '/groups/1234/action', body=expected)
+
+ @ddt.data((False, None), (True, None),
+ (False, "backend1"), (True, "backend1"),
+ (False, "default"), (True, "default"))
+ @ddt.unpack
+ def test_group_failover_replication(self, attach_vol, backend):
+ attach = '--allow-attached-volume ' if attach_vol else ''
+ backend_id = ('--secondary-backend-id ' + backend) if backend else ''
+ cmd = ('--os-volume-api-version 3.38 group-failover-replication 1234 '
+ + attach + backend_id)
+ self.run_command(cmd)
+ expected = {'failover_replication':
+ {'allow_attached_volume': attach_vol,
+ 'secondary_backend_id': backend if backend else None}}
+ self.assert_called('POST', '/groups/1234/action', body=expected)
+
+ def test_group_list_replication_targets(self):
+ cmd = ('--os-volume-api-version 3.38 group-list-replication-targets'
+ ' 1234')
+ self.run_command(cmd)
+ expected = {'list_replication_targets': {}}
+ self.assert_called('POST', '/groups/1234/action', body=expected)
+
+ @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels')
+ def test_service_get_log_before_3_32(self, get_levels_mock):
+ self.assertRaises(SystemExit,
+ self.run_command, '--os-volume-api-version 3.28 '
+ 'service-get-log')
+ get_levels_mock.assert_not_called()
+
+ @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels')
+ @mock.patch('cinderclient.utils.print_list')
+ def test_service_get_log_no_params(self, print_mock, get_levels_mock):
+ self.run_command('--os-volume-api-version 3.32 service-get-log')
+ get_levels_mock.assert_called_once_with('', '', '')
+ print_mock.assert_called_once_with(get_levels_mock.return_value,
+ ('Binary', 'Host', 'Prefix',
+ 'Level'))
+
+ @ddt.data('*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
+ 'cinder-backup')
+ @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels')
+ @mock.patch('cinderclient.utils.print_list')
+ def test_service_get_log(self, binary, print_mock, get_levels_mock):
+ server = 'host1'
+ prefix = 'sqlalchemy'
+
+ self.run_command('--os-volume-api-version 3.32 service-get-log '
+ '--binary %s --server %s --prefix %s' % (
+ binary, server, prefix))
+ get_levels_mock.assert_called_once_with(binary, server, prefix)
+ print_mock.assert_called_once_with(get_levels_mock.return_value,
+ ('Binary', 'Host', 'Prefix',
+ 'Level'))
+
+ @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels')
+ def test_service_set_log_before_3_32(self, set_levels_mock):
+ self.assertRaises(SystemExit,
+ self.run_command, '--os-volume-api-version 3.28 '
+ 'service-set-log debug')
+ set_levels_mock.assert_not_called()
+
+ @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels')
+ @mock.patch('cinderclient.shell.CinderClientArgumentParser.error')
+ def test_service_set_log_missing_required(self, error_mock,
+ set_levels_mock):
+ error_mock.side_effect = SystemExit
+ self.assertRaises(SystemExit,
+ self.run_command, '--os-volume-api-version 3.32 '
+ 'service-set-log')
+ set_levels_mock.assert_not_called()
+ # Different error message from argparse library in Python 2 and 3
+ if six.PY3:
+ msg = 'the following arguments are required: <log-level>'
+ else:
+ msg = 'too few arguments'
+ error_mock.assert_called_once_with(msg)
+
+ @ddt.data('debug', 'DEBUG', 'info', 'INFO', 'warning', 'WARNING', 'error',
+ 'ERROR')
+ @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels')
+ def test_service_set_log_min_params(self, level, set_levels_mock):
+ self.run_command('--os-volume-api-version 3.32 '
+ 'service-set-log %s' % level)
+ set_levels_mock.assert_called_once_with(level, '', '', '')
+
+ @ddt.data('*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
+ 'cinder-backup')
+ @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels')
+ def test_service_set_log_levels(self, binary, set_levels_mock):
+ level = 'debug'
+ server = 'host1'
+ prefix = 'sqlalchemy.'
+ self.run_command('--os-volume-api-version 3.32 '
+ 'service-set-log %s --binary %s --server %s '
+ '--prefix %s' % (level, binary, server, prefix))
+ set_levels_mock.assert_called_once_with(level, binary, server, prefix)
+
+ @mock.patch('cinderclient.shell_utils._poll_for_status')
+ def test_create_with_poll(self, poll_method):
+ self.run_command('create --poll 1')
+ self.assert_called_anytime('GET', '/volumes/1234')
+ volume = self.shell.cs.volumes.get('1234')
+ info = dict()
+ info.update(volume._info)
+ info.pop('links', None)
+ self.assertEqual(1, poll_method.call_count)
+ timeout_period = 3600
+ poll_method.assert_has_calls([mock.call(self.shell.cs.volumes.get,
+ 1234, info, 'creating', ['available'], timeout_period,
+ self.shell.cs.client.global_request_id,
+ self.shell.cs.messages)])
+
+ @mock.patch('cinderclient.shell_utils.time')
+ def test_poll_for_status(self, mock_time):
+ poll_period = 2
+ some_id = "some-id"
+ global_request_id = "req-someid"
+ action = "some"
+ updated_objects = (
+ base.Resource(None, info={"not_default_field": "creating"}),
+ base.Resource(None, info={"not_default_field": "available"}))
+ poll_fn = mock.MagicMock(side_effect=updated_objects)
+ cinderclient.shell_utils._poll_for_status(
+ poll_fn = poll_fn,
+ obj_id = some_id,
+ global_request_id = global_request_id,
+ messages = base.Resource(None, {}),
+ info = {},
+ action = action,
+ status_field = "not_default_field",
+ final_ok_states = ['available'],
+ timeout_period=3600)
+ self.assertEqual([mock.call(poll_period)] * 2,
+ mock_time.sleep.call_args_list)
+ self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list)
+
+ @mock.patch('cinderclient.v3.messages.MessageManager.list')
+ @mock.patch('cinderclient.shell_utils.time')
+ def test_poll_for_status_error(self, mock_time, mock_message_list):
+ poll_period = 2
+ some_id = "some_id"
+ global_request_id = "req-someid"
+ action = "some"
+ updated_objects = (
+ base.Resource(None, info={"not_default_field": "creating"}),
+ base.Resource(None, info={"not_default_field": "error"}))
+ poll_fn = mock.MagicMock(side_effect=updated_objects)
+ msg_object = base.Resource(cinderclient.v3.messages.MessageManager,
+ info = {"user_message": "ERROR!"})
+ mock_message_list.return_value = (msg_object,)
+ self.assertRaises(exceptions.ResourceInErrorState,
+ cinderclient.shell_utils._poll_for_status,
+ poll_fn=poll_fn,
+ obj_id=some_id,
+ global_request_id=global_request_id,
+ messages=cinderclient.v3.messages.MessageManager(api=3.34),
+ info=dict(),
+ action=action,
+ final_ok_states=['available'],
+ status_field="not_default_field",
+ timeout_period=3600)
+ self.assertEqual([mock.call(poll_period)] * 2,
+ mock_time.sleep.call_args_list)
+ self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list)
diff --git a/cinderclient/tests/unit/v3/test_volumes.py b/cinderclient/tests/unit/v3/test_volumes.py
index 3a84bf3..72ef916 100644
--- a/cinderclient/tests/unit/v3/test_volumes.py
+++ b/cinderclient/tests/unit/v3/test_volumes.py
@@ -18,9 +18,11 @@
import ddt
from cinderclient import api_versions
+from cinderclient import exceptions
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v3 import fakes
from cinderclient.v3 import volumes
+from cinderclient.v3 import volume_snapshots
from six.moves.urllib import parse
@@ -48,6 +50,28 @@ class VolumesTest(utils.TestCase):
visibility='public', protected=True)
cs.assert_called_anytime('POST', '/volumes/1234/action', body=expected)
+ @ddt.data('3.39', '3.40')
+ def test_revert_to_snapshot(self, version):
+
+ api_version = api_versions.APIVersion(version)
+ cs = fakes.FakeClient(api_version)
+ manager = volumes.VolumeManager(cs)
+ fake_snapshot = volume_snapshots.Snapshot(
+ manager, {'id': 12345, 'name': 'fake-snapshot'}, loaded=True)
+ fake_volume = volumes.Volume(manager,
+ {'id': 1234, 'name': 'sample-volume'},
+ loaded=True)
+ expected = {'revert': {'snapshot_id': 12345}}
+
+ if version == '3.40':
+ fake_volume.revert_to_snapshot(fake_snapshot)
+
+ cs.assert_called_anytime('POST', '/volumes/1234/action',
+ body=expected)
+ else:
+ self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
+ fake_volume.revert_to_snapshot, fake_snapshot)
+
def test_create_volume(self):
vol = cs.volumes.create(1, group_id='1234', volume_type='5678')
expected = {'volume': {'status': 'creating',
@@ -70,6 +94,14 @@ class VolumesTest(utils.TestCase):
cs.assert_called('POST', '/volumes', body=expected)
self._assert_request_id(vol)
+ @ddt.data((False, '/volumes/summary'),
+ (True, '/volumes/summary?all_tenants=True'))
+ def test_volume_summary(self, all_tenants_input):
+ all_tenants, url = all_tenants_input
+ cs = fakes.FakeClient(api_versions.APIVersion('3.12'))
+ cs.volumes.summary(all_tenants=all_tenants)
+ cs.assert_called('GET', url)
+
def test_volume_list_manageable(self):
cs = fakes.FakeClient(api_versions.APIVersion('3.8'))
cs.volumes.list_manageable('host1', detailed=False)
diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py
index 2c68d6d..e1b757f 100644
--- a/cinderclient/v2/shell.py
+++ b/cinderclient/v2/shell.py
@@ -32,6 +32,18 @@ from cinderclient import utils
from cinderclient.v2 import availability_zones
+def _translate_attachments(info):
+ attachments = []
+ attached_servers = []
+ for attachment in info['attachments']:
+ attachments.append(attachment['id'])
+ attached_servers.append(attachment['server_id'])
+ info.pop('attachments', None)
+ info['attachment_ids'] = attachments
+ info['attached_servers'] = attached_servers
+ return info
+
+
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
@@ -188,9 +200,10 @@ def do_show(cs, args):
info['readonly'] = info['metadata']['readonly']
info.pop('links', None)
+ info = _translate_attachments(info)
utils.print_dict(info,
formatters=['metadata', 'volume_image_metadata',
- 'attachments'])
+ 'attachment_ids', 'attached_servers'])
class CheckSizeArgForCreate(argparse.Action):
@@ -340,6 +353,7 @@ def do_create(cs, args):
info['readonly'] = info['metadata']['readonly']
info.pop('links', None)
+ info = _translate_attachments(info)
utils.print_dict(info)
@@ -717,6 +731,8 @@ def do_snapshot_rename(cs, args):
raise exceptions.ClientException(code=1, message=msg)
shell_utils.find_volume_snapshot(cs, args.snapshot).update(**kwargs)
+ print("Request to rename snapshot '%s' has been accepted." % (
+ args.snapshot))
@utils.arg('snapshot', metavar='<snapshot>', nargs='+',
@@ -976,10 +992,6 @@ def do_quota_defaults(cs, args):
metavar='<backup_gigabytes>',
type=int, default=None,
help='The new "backup_gigabytes" quota value. Default=None.')
-@utils.arg('--consistencygroups',
- metavar='<consistencygroups>',
- type=int, default=None,
- help='The new "consistencygroups" quota value. Default=None.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
@@ -1397,6 +1409,7 @@ def do_backup_reset_state(cs, args):
for backup in args.backup:
try:
shell_utils.find_backup(cs, backup).reset_state(args.state)
+ print("Request to update backup '%s' has been accepted." % backup)
except Exception as e:
failure_count += 1
msg = "Reset state for backup %s failed: %s" % (backup, e)
@@ -2236,6 +2249,8 @@ def do_consisgroup_update(cs, args):
shell_utils.find_consistencygroup(
cs, args.consistencygroup).update(**kwargs)
+ print("Request to update consistency group '%s' has been accepted." % (
+ args.consistencygroup))
@utils.arg('--all-tenants',
diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py
index 02b86ee..b4e950b 100644
--- a/cinderclient/v2/volumes.py
+++ b/cinderclient/v2/volumes.py
@@ -33,7 +33,17 @@ class Volume(base.Resource):
return self.manager.update(self, **kwargs)
def attach(self, instance_uuid, mountpoint, mode='rw', host_name=None):
- """Set attachment metadata.
+ """Inform Cinder that the given volume is attached to the given instance.
+
+ Calling this method will not actually ask Cinder to attach
+ a volume, but to mark it on the DB as attached. If the volume
+ is not actually attached to the given instance, inconsistent
+ data will result.
+
+ The right flow of calls is :
+ 1- call reserve
+ 2- call initialize_connection
+ 3- call attach
:param instance_uuid: uuid of the attaching instance.
:param mountpoint: mountpoint on the attaching instance or host.
@@ -44,7 +54,18 @@ class Volume(base.Resource):
host_name)
def detach(self):
- """Clear attachment metadata."""
+ """Inform Cinder that the given volume is detached from the given instance.
+
+ Calling this method will not actually ask Cinder to detach
+ a volume, but to mark it on the DB as detached. If the volume
+ is not actually detached from the given instance, inconsistent
+ data will result.
+
+ The right flow of calls is :
+ 1- call reserve
+ 2- call initialize_connection
+ 3- call detach
+ """
return self.manager.detach(self)
def reserve(self, volume):
diff --git a/cinderclient/v3/groups.py b/cinderclient/v3/groups.py
index 386a6a3..6bc298a 100644
--- a/cinderclient/v3/groups.py
+++ b/cinderclient/v3/groups.py
@@ -39,6 +39,25 @@ class Group(base.Resource):
"""Reset the group's state with specified one"""
return self.manager.reset_state(self, state)
+ def enable_replication(self):
+ """Enables replication for this group."""
+ return self.manager.enable_replication(self)
+
+ def disable_replication(self):
+ """Disables replication for this group."""
+ return self.manager.disable_replication(self)
+
+ def failover_replication(self, allow_attached_volume=False,
+ secondary_backend_id=None):
+ """Fails over replication for this group."""
+ return self.manager.failover_replication(self,
+ allow_attached_volume,
+ secondary_backend_id)
+
+ def list_replication_targets(self):
+ """Lists replication targets for this group."""
+ return self.manager.list_replication_targets(self)
+
class GroupManager(base.ManagerWithFind):
"""Manage :class:`Group` resources."""
@@ -180,3 +199,55 @@ class GroupManager(base.ManagerWithFind):
url = '/groups/%s/action' % base.getid(group)
resp, body = self.api.client.post(url, body=body)
return common_base.TupleWithMeta((resp, body), resp)
+
+ def enable_replication(self, group):
+ """Enables replication for a group.
+
+ :param group: the :class:`Group` to enable replication.
+ """
+ body = {'enable_replication': {}}
+ self.run_hooks('modify_body_for_action', body, 'group')
+ url = '/groups/%s/action' % base.getid(group)
+ resp, body = self.api.client.post(url, body=body)
+ return common_base.TupleWithMeta((resp, body), resp)
+
+ def disable_replication(self, group):
+ """disables replication for a group.
+
+ :param group: the :class:`Group` to disable replication.
+ """
+ body = {'disable_replication': {}}
+ self.run_hooks('modify_body_for_action', body, 'group')
+ url = '/groups/%s/action' % base.getid(group)
+ resp, body = self.api.client.post(url, body=body)
+ return common_base.TupleWithMeta((resp, body), resp)
+
+ def failover_replication(self, group, allow_attached_volume=False,
+ secondary_backend_id=None):
+ """fails over replication for a group.
+
+ :param group: the :class:`Group` to failover.
+ :param allow attached volumes: allow attached volumes in the group.
+ :param secondary_backend_id: secondary backend id.
+ """
+ body = {
+ 'failover_replication': {
+ 'allow_attached_volume': allow_attached_volume,
+ 'secondary_backend_id': secondary_backend_id
+ }
+ }
+ self.run_hooks('modify_body_for_action', body, 'group')
+ url = '/groups/%s/action' % base.getid(group)
+ resp, body = self.api.client.post(url, body=body)
+ return common_base.TupleWithMeta((resp, body), resp)
+
+ def list_replication_targets(self, group):
+ """List replication targets for a group.
+
+ :param group: the :class:`Group` to list replication targets.
+ """
+ body = {'list_replication_targets': {}}
+ self.run_hooks('modify_body_for_action', body, 'group')
+ url = '/groups/%s/action' % base.getid(group)
+ resp, body = self.api.client.post(url, body=body)
+ return common_base.TupleWithMeta((resp, body), resp)
diff --git a/cinderclient/v3/services.py b/cinderclient/v3/services.py
index bd8a6c4..55d3ee4 100644
--- a/cinderclient/v3/services.py
+++ b/cinderclient/v3/services.py
@@ -18,11 +18,18 @@ service interface
"""
from cinderclient import api_versions
+from cinderclient import base
from cinderclient.v2 import services
Service = services.Service
+class LogLevel(base.Resource):
+ def __repr__(self):
+ return '<LogLevel: binary=%s host=%s prefix=%s level=%s>' % (
+ self.binary, self.host, self.prefix, self.level)
+
+
class ServiceManager(services.ServiceManager):
@api_versions.wraps("3.0")
def server_api_version(self):
@@ -36,3 +43,25 @@ class ServiceManager(services.ServiceManager):
return self._get_with_base_url("", response_key='versions')
except LookupError:
return []
+
+ @api_versions.wraps("3.32")
+ def set_log_levels(self, level, binary, server, prefix):
+ """Set log level for services."""
+ body = {'level': level, 'binary': binary, 'server': server,
+ 'prefix': prefix}
+ return self._update("/os-services/set-log", body)
+
+ @api_versions.wraps("3.32")
+ def get_log_levels(self, binary, server, prefix):
+ """Get log levels for services."""
+ body = {'binary': binary, 'server': server, 'prefix': prefix}
+ response = self._update("/os-services/get-log", body)
+
+ log_levels = []
+ for entry in response['log_levels']:
+ entry_levels = sorted(entry['levels'].items(), key=lambda x: x[0])
+ for prefix, level in entry_levels:
+ log_dict = {'binary': entry['binary'], 'host': entry['host'],
+ 'prefix': prefix, 'level': level}
+ log_levels.append(LogLevel(self, log_dict, loaded=True))
+ return log_levels
diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py
index 6a1e3b9..f47b83c 100644
--- a/cinderclient/v3/shell.py
+++ b/cinderclient/v3/shell.py
@@ -19,6 +19,7 @@ from __future__ import print_function
import argparse
import collections
import os
+import sys
from oslo_utils import strutils
import six
@@ -98,7 +99,7 @@ def do_list_filters(cs, args):
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -139,7 +140,7 @@ def do_backup_list(cs, args):
action='store_true',
help='Show detailed information about pools.')
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -273,7 +274,7 @@ RESET_STATE_RESOURCES = {'volume': utils.find_volume,
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -503,6 +504,9 @@ def do_reset_state(cs, args):
help=('Allow volume to be attached more than once.'
' Default=False'),
default=False)
+@utils.arg('--poll',
+ action="store_true",
+ help=('Wait for volume creation until it completes.'))
def do_create(cs, args):
"""Creates a volume."""
@@ -563,6 +567,12 @@ def do_create(cs, args):
info['readonly'] = info['metadata']['readonly']
info.pop('links', None)
+
+ if args.poll:
+ timeout_period = os.environ.get("POLL_TIMEOUT_PERIOD", 3600)
+ shell_utils._poll_for_status(cs.volumes.get, volume.id, info, 'creating', ['available'],
+ timeout_period, cs.client.global_request_id, cs.messages)
+
utils.print_dict(info)
@@ -600,6 +610,27 @@ def do_metadata(cs, args):
reverse=True))
+@api_versions.wraps('3.12')
+@utils.arg('--all-tenants',
+ dest='all_tenants',
+ metavar='<0|1>',
+ nargs='?',
+ type=int,
+ const=1,
+ default=utils.env('ALL_TENANTS', default=0),
+ help='Shows details for all tenants. Admin only.')
+def do_summary(cs, args):
+ """Get volumes summary."""
+ all_tenants = args.all_tenants
+ info = cs.volumes.summary(all_tenants)
+
+ formatters = ['total_size', 'total_count']
+ if cs.api_version >= api_versions.APIVersion("3.36"):
+ formatters.append('metadata')
+
+ utils.print_dict(info['volume-summary'], formatters=formatters)
+
+
@api_versions.wraps('3.11')
def do_group_type_list(cs, args):
"""Lists available 'group types'. (Admin only will see private types)"""
@@ -743,10 +774,6 @@ def do_group_type_key(cs, args):
metavar='<backup_gigabytes>',
type=int, default=None,
help='The new "backup_gigabytes" quota value. Default=None.')
-@utils.arg('--consistencygroups',
- metavar='<consistencygroups>',
- type=int, default=None,
- help='The new "consistencygroups" quota value. Default=None.')
@utils.arg('--groups',
metavar='<groups>',
type=int, default=None,
@@ -849,6 +876,7 @@ def do_backup_update(cs, args):
raise exceptions.ClientException(code=1, message=msg)
shell_utils.find_backup(cs, args.backup).update(**kwargs)
+ print("Request to update backup '%s' has been accepted." % args.backup)
@api_versions.wraps('3.7')
@@ -976,7 +1004,7 @@ def do_manageable_list(cs, args):
default=utils.env('ALL_TENANTS', default=0),
help='Shows details for all tenants. Admin only.')
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -1170,9 +1198,77 @@ def do_group_update(cs, args):
raise exceptions.ClientException(code=1, message=msg)
shell_utils.find_group(cs, args.group).update(**kwargs)
+ print("Request to update group '%s' has been accepted." % args.group)
+
+
+@api_versions.wraps('3.38')
+@utils.arg('group',
+ metavar='<group>',
+ help='Name or ID of the group.')
+def do_group_enable_replication(cs, args):
+ """Enables replication for group."""
+
+ shell_utils.find_group(cs, args.group).enable_replication()
+
+
+@api_versions.wraps('3.38')
+@utils.arg('group',
+ metavar='<group>',
+ help='Name or ID of the group.')
+def do_group_disable_replication(cs, args):
+ """Disables replication for group."""
+
+ shell_utils.find_group(cs, args.group).disable_replication()
+
+
+@api_versions.wraps('3.38')
+@utils.arg('group',
+ metavar='<group>',
+ help='Name or ID of the group.')
+@utils.arg('--allow-attached-volume',
+ action='store_true',
+ default=False,
+ help='Allows or disallows group with '
+ 'attached volumes to be failed over.')
+@utils.arg('--secondary-backend-id',
+ metavar='<secondary_backend_id>',
+ help='Secondary backend id. Default=None.')
+def do_group_failover_replication(cs, args):
+ """Fails over replication for group."""
+
+ shell_utils.find_group(cs, args.group).failover_replication(
+ allow_attached_volume=args.allow_attached_volume,
+ secondary_backend_id=args.secondary_backend_id)
+
+
+@api_versions.wraps('3.38')
+@utils.arg('group',
+ metavar='<group>',
+ help='Name or ID of the group.')
+def do_group_list_replication_targets(cs, args):
+ """Lists replication targets for group.
+
+ Example value for replication_targets:
+
+ .. code-block: json
+
+ {
+ 'replication_targets': [{'backend_id': 'vendor-id-1',
+ 'unique_key': 'val1',
+ ......},
+ {'backend_id': 'vendor-id-2',
+ 'unique_key': 'val2',
+ ......}]
+ }
+ """
+
+ rc, replication_targets = shell_utils.find_group(
+ cs, args.group).list_replication_targets()
+ rep_targets = replication_targets.get('replication_targets')
+ if rep_targets and len(rep_targets) > 0:
+ utils.print_list(rep_targets, [key for key in rep_targets[0].keys()])
-@api_versions.wraps('3.14')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
@@ -1192,7 +1288,7 @@ def do_group_update(cs, args):
help="Filters results by a group ID. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -1371,6 +1467,19 @@ def do_api_version(cs, args):
utils.print_list(response, columns)
+@api_versions.wraps("3.40")
+@utils.arg(
+ 'snapshot',
+ metavar='<snapshot>',
+ help='Name or ID of the snapshot to restore. The snapshot must be the '
+ 'most recent one known to cinder.')
+def do_revert_to_snapshot(cs, args):
+ """Revert a volume to the specified snapshot."""
+ snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot)
+ volume = utils.find_volume(cs, snapshot.volume_id)
+ volume.revert_to_snapshot(snapshot)
+
+
@api_versions.wraps("3.3")
@utils.arg('--marker',
metavar='<marker>',
@@ -1418,7 +1527,7 @@ def do_api_version(cs, args):
help="Filters results by the message level. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -1556,7 +1665,7 @@ def do_message_delete(cs, args):
"volume api version >=3.22. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -1647,7 +1756,7 @@ def do_snapshot_list(cs, args):
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
- type=str,
+ type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
@@ -1750,7 +1859,8 @@ def do_attachment_create(cs, args):
'os_type': args.ostype,
'multipath': args.multipath,
'mountpoint': args.mountpoint}
- attachment = cs.attachments.create(args.volume,
+ volume = utils.find_volume(cs, args.volume)
+ attachment = cs.attachments.create(volume.id,
connector,
args.server_id)
connector_dict = attachment.pop('connection_info', None)
@@ -1840,3 +1950,44 @@ def do_version_list(cs, args):
print("\nServer supported API versions:")
utils.print_list(result, columns)
+
+
+@api_versions.wraps('3.32')
+@utils.arg('level',
+ metavar='<log-level>',
+ choices=('INFO', 'WARNING', 'ERROR', 'DEBUG',
+ 'info', 'warning', 'error', 'debug'),
+ help='Desired log level.')
+@utils.arg('--binary',
+ choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
+ 'cinder-backup'),
+ default='',
+ help='Binary to change.')
+@utils.arg('--server',
+ default='',
+ help='Host or cluster value for service.')
+@utils.arg('--prefix',
+ default='',
+ help='Prefix for the log. ie: "cinder.volume.drivers.".')
+def do_service_set_log(cs, args):
+ cs.services.set_log_levels(args.level, args.binary, args.server,
+ args.prefix)
+
+
+@api_versions.wraps('3.32')
+@utils.arg('--binary',
+ choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
+ 'cinder-backup'),
+ default='',
+ help='Binary to query.')
+@utils.arg('--server',
+ default='',
+ help='Host or cluster value for service.')
+@utils.arg('--prefix',
+ default='',
+ help='Prefix for the log. ie: "sqlalchemy.".')
+def do_service_get_log(cs, args):
+ log_levels = cs.services.get_log_levels(args.binary, args.server,
+ args.prefix)
+ columns = ('Binary', 'Host', 'Prefix', 'Level')
+ utils.print_list(log_levels, columns)
diff --git a/cinderclient/v3/volumes.py b/cinderclient/v3/volumes.py
index 2224489..2e4eadc 100644
--- a/cinderclient/v3/volumes.py
+++ b/cinderclient/v3/volumes.py
@@ -45,6 +45,10 @@ class Volume(volumes.Volume):
return self.manager.upload_to_image(self, force, image_name,
container_format, disk_format)
+ def revert_to_snapshot(self, snapshot):
+ """Revert a volume to a snapshot."""
+ self.manager.revert_to_snapshot(self, snapshot)
+
class VolumeManager(volumes.VolumeManager):
resource_class = Volume
@@ -109,6 +113,25 @@ class VolumeManager(volumes.VolumeManager):
return self._create('/volumes', body, 'volume')
+ @api_versions.wraps('3.40')
+ def revert_to_snapshot(self, volume, snapshot):
+ """Revert a volume to a snapshot.
+
+ The snapshot must be the most recent one known to cinder.
+ :param volume: volume object.
+ :param snapshot: snapshot object.
+ """
+ return self._action('revert', volume,
+ info={'snapshot_id': base.getid(snapshot.id)})
+
+ def summary(self, all_tenants):
+ """Get volumes summary."""
+ url = "/volumes/summary"
+ if all_tenants:
+ url += "?all_tenants=True"
+ _, body = self.api.client.get(url)
+ return body
+
@api_versions.wraps("3.0")
def delete_metadata(self, volume, keys):
"""Delete specified keys from volumes metadata.
@@ -131,6 +154,7 @@ class VolumeManager(volumes.VolumeManager):
:param volume: The :class:`Volume`.
:param keys: A list of keys to be removed.
"""
+ # pylint: disable=function-redefined
data = self._get("/volumes/%s/metadata" % base.getid(volume))
metadata = data._info.get("metadata", {})
if set(keys).issubset(metadata.keys()):
@@ -160,6 +184,7 @@ class VolumeManager(volumes.VolumeManager):
"""Upload volume to image service as image.
:param volume: The :class:`Volume` to upload.
"""
+ # pylint: disable=function-redefined
return self._action('os-volume_upload_image',
volume,
{'force': force,
diff --git a/doc/source/conf.py b/doc/source/conf.py
index e19f38b..1af8db5 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -43,7 +43,7 @@ sys.path.insert(0, ROOT)
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
- 'oslosphinx',
+ 'openstackdocstheme',
'reno.sphinxext',
]
@@ -120,6 +120,7 @@ man_pages = [
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme = 'nature'
+html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -188,6 +189,10 @@ html_static_path = ['_static']
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-cinderclientdoc'
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
# -- Options for LaTeX output -------------------------------------------------
@@ -220,4 +225,9 @@ latex_documents = [
# latex_appendices = []
# If false, no module index is generated.
-# latex_use_modindex = True \ No newline at end of file
+# latex_use_modindex = True
+
+# -- Options for openstackdocstheme -------------------------------------------
+repository_name = 'openstack/python-cinderclient'
+bug_project = 'cinderclient'
+bug_tag = ''
diff --git a/doc/source/functional_tests.rst b/doc/source/functional_tests.rst
index 3c90a40..6af85ba 100644
--- a/doc/source/functional_tests.rst
+++ b/doc/source/functional_tests.rst
@@ -38,7 +38,7 @@ Or all tests in the test_readonly_clitest_readonly_cli.py file::
tox -e functional -- -n cinderclient.tests.functional.test_readonly_cli
For more information on these options and how to run tests, please see the
-`ostestr documentation <http://docs.openstack.org/developer/os-testr/>`_.
+`ostestr documentation <https://docs.openstack.org/os-testr/latest/>`_.
Gotchas
-------
diff --git a/doc/source/index.rst b/doc/source/index.rst
index a87f414..4aaf66b 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -47,7 +47,7 @@ Release Notes
All python-cinderclient release notes can now be found on the `release notes`_ page.
-.. _`release notes`: http://docs.openstack.org/releasenotes/python-cinderclient/
+.. _`release notes`: https://docs.openstack.org/releasenotes/python-cinderclient/
The following are kept for historical purposes.
diff --git a/doc/source/unit_tests.rst b/doc/source/unit_tests.rst
index 38fb4e2..988740d 100644
--- a/doc/source/unit_tests.rst
+++ b/doc/source/unit_tests.rst
@@ -39,7 +39,7 @@ Or all tests in the test_volumes.py file::
tox -epy27 -- -n cinderclient.tests.unit.v2.test_volumes
For more information on these options and how to run tests, please see the
-`ostestr documentation <http://docs.openstack.org/developer/os-testr/>`_.
+`ostestr documentation <https://docs.openstack.org/os-testr/latest/>`_.
Run tests wrapper script
------------------------
@@ -94,7 +94,7 @@ This will show the following help information::
Because ``run_tests.sh`` is a wrapper around testr, it also accepts the same
flags as testr. See the documentation for details about these additional flags:
-`ostestr documentation <http://docs.openstack.org/developer/os-testr/>`_.
+`ostestr documentation <https://docs.openstack.org/os-testr/latest/>`_.
.. _nose options documentation: http://readthedocs.org/docs/nose/en/latest/usage.html#options
diff --git a/releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml b/releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml
new file mode 100644
index 0000000..ee7ef73
--- /dev/null
+++ b/releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml
@@ -0,0 +1,10 @@
+---
+fixes:
+ - |
+ Fixes `bug 1705093`_ by having the
+ ``cinderclient.client.get_highest_client_server_version`` method return a
+ string rather than a float. The problem with returning a float is when a
+ user of that method would cast the float result to a str which turns 3.40,
+ for example, into "3.4" which is wrong.
+
+ .. _bug 1705093: https://bugs.launchpad.net/python-cinderclient/+bug/1705093
diff --git a/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml b/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml
new file mode 100644
index 0000000..5d6a106
--- /dev/null
+++ b/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml
@@ -0,0 +1,4 @@
+features:
+ - |
+ Support to wait for volume creation until it completes.
+ The command is: ``cinder create --poll <volume_size>``
diff --git a/releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml b/releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml
new file mode 100644
index 0000000..6f4d4d4
--- /dev/null
+++ b/releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml
@@ -0,0 +1,3 @@
+---
+other:
+ - The useless consistencygroup quota operation has been removed.
diff --git a/releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml b/releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml
new file mode 100644
index 0000000..43e0cd0
--- /dev/null
+++ b/releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Added support for replication group APIs ``enable_replication``,
+ ``disable_replication``, ``failover_replication`` and
+ ``list_replication_targets``.
diff --git a/releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml b/releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml
new file mode 100644
index 0000000..395fab3
--- /dev/null
+++ b/releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Added support for the revert-to-snapshot feature.
diff --git a/releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml b/releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml
new file mode 100644
index 0000000..e71c7db
--- /dev/null
+++ b/releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Support microversion 3.32 that allows dynamically changing and querying
+ Cinder services' log levels with ``service-set-log`` and
+ ``service-get-log`` commands.
diff --git a/releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml b/releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml
new file mode 100644
index 0000000..a9856e1
--- /dev/null
+++ b/releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Support get volume summary command in V3.12.
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
index ffd5111..2623c9e 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -38,7 +38,7 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'oslosphinx',
+ 'openstackdocstheme',
'reno.sphinxext',
]
@@ -112,7 +112,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -151,6 +151,7 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
@@ -277,3 +278,8 @@ texinfo_documents = [
# -- Options for Internationalization output ------------------------------
locale_dirs = ['locale/']
+
+# -- Options for openstackdocstheme -------------------------------------------
+repository_name = 'openstack/python-cinderclient'
+bug_project = 'cinderclient'
+bug_tag = ''
diff --git a/requirements.txt b/requirements.txt
index 9fab454..5e72d7c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
-keystoneauth1>=2.21.0 # Apache-2.0
+keystoneauth1>=3.0.1 # Apache-2.0
simplejson>=2.2.0 # MIT
Babel!=2.4.0,>=2.3.4 # BSD
six>=1.9.0 # MIT
diff --git a/test-requirements.txt b/test-requirements.txt
index df6cdcb..3226d7d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,13 +7,13 @@ coverage!=4.4,>=4.0 # Apache-2.0
ddt>=1.0.1 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
mock>=2.0 # BSD
-oslosphinx>=4.7.0 # Apache-2.0
+openstackdocstheme>=1.11.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
reno!=2.3.1,>=1.8.0 # Apache-2.0
requests-mock>=1.1 # Apache-2.0
-sphinx!=1.6.1,>=1.5.1 # BSD
-tempest>=14.0.0 # Apache-2.0
+sphinx>=1.6.2 # BSD
+tempest>=16.1.0 # Apache-2.0
testtools>=1.4.0 # MIT
testrepository>=0.0.18 # Apache-2.0/BSD
os-testr>=0.8.0 # Apache-2.0
-oslo.serialization>=1.10.0 # Apache-2.0
+oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0