From 2ed5cdcb5f702a036be46c354b1511aa26f7c940 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 6 May 2013 18:25:26 -0500 Subject: [PATCH 01/51] Add license information. Several files were missing the license issue, so simply add them. Change-Id: I866ec03096a72fe8ae7d776e2ffe040379ec5bc6 Signed-off-by: Chuck Short --- cinderclient/client.py | 16 ++++++++++++++-- cinderclient/utils.py | 15 +++++++++++++++ cinderclient/v1/client.py | 15 +++++++++++++++ cinderclient/v1/contrib/__init__.py | 14 ++++++++++++++ cinderclient/v2/client.py | 15 +++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 3a4a296..8e0d323 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -1,8 +1,20 @@ -# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack LLC. +# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Piston Cloud Computing, 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. + """ OpenStack Client interface. Handles the REST calls and responses. """ diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 558c292..1c26656 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -1,3 +1,18 @@ +# Copyright 2013 OpenStack LLC +# 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. + import os import re import sys diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 9d71b60..a5b9b02 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -1,3 +1,18 @@ +# Copyright 2013 OpenStack LLC +# 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 import client from cinderclient.v1 import limits from cinderclient.v1 import quota_classes diff --git a/cinderclient/v1/contrib/__init__.py b/cinderclient/v1/contrib/__init__.py index e69de29..dc6c3a3 100644 --- a/cinderclient/v1/contrib/__init__.py +++ b/cinderclient/v1/contrib/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack LLC. +# 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. diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 9e17718..eb2760c 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -1,3 +1,18 @@ +# Copyright 2013 OpenStack LLC. +# 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 import client from cinderclient.v2 import limits from cinderclient.v2 import quota_classes From a122a76b3d20573ca3082ecfcaa6499e8fbba400 Mon Sep 17 00:00:00 2001 From: Alexey Ovchinnikov Date: Tue, 14 May 2013 17:07:29 +0400 Subject: [PATCH 02/51] Fixed do_create() in v2 shell. Previously when creating a volume v2 shell was able to create it but then failed to display volume properties. Instead of that it displayed a non-informative error message. This patch fixes the problem. Change-Id: I1d3f9127ddd793a905527660b64ffe8c28f20f1d --- cinderclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 081fe23..a502378 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -248,7 +248,7 @@ def do_create(cs, args): metadata=volume_metadata) info = dict() - volume = cs.volumes.get(info['id']) + volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links') From 95e142a1738cc01a84de427a7c96435a5b01cbf3 Mon Sep 17 00:00:00 2001 From: Hugh Saunders Date: Tue, 14 May 2013 20:16:35 +0100 Subject: [PATCH 03/51] Allow generator as input to utils.print_list. Once the table is built, the length of the prettytable's internal array is checked rather than re-iterrating over the input. Adds tests for utils.print_list with list and generator input. Change-Id: I4c0bd08bf0c943de42ad90d255a2d831c2e98828 Fixes: bug #1180059 --- cinderclient/utils.py | 2 +- tests/test_utils.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 1c26656..1a0034a 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -158,7 +158,7 @@ def print_list(objs, fields, formatters={}): row.append(data) pt.add_row(row) - if len(objs) > 0: + if len(pt._rows) > 0: print strutils.safe_encode(pt.get_string(sortby=fields[0])) diff --git a/tests/test_utils.py b/tests/test_utils.py index 22a167a..af3f2dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,6 @@ +import collections +import StringIO +import sys from cinderclient import exceptions from cinderclient import utils @@ -73,3 +76,51 @@ class FindResourceTestCase(test_utils.TestCase): def test_find_by_str_displayname(self): output = utils.find_resource(self.manager, 'entity_three') self.assertEqual(output, self.manager.get('4242')) + + +class CaptureStdout(object): + """Context manager for capturing stdout from statments in its's block""" + def __enter__(self): + self.real_stdout = sys.stdout + self.stringio = StringIO.StringIO() + sys.stdout = self.stringio + return self + + def __exit__(self, *args): + sys.stdout = self.real_stdout + self.stringio.seek(0) + self.read = self.stringio.read + + +class PrintListTestCase(test_utils.TestCase): + + def test_print_list_with_list(self): + Row = collections.namedtuple('Row', ['a', 'b']) + to_print = [Row(a=1, b=2), Row(a=3, b=4)] + with CaptureStdout() as cso: + utils.print_list(to_print, ['a', 'b']) + self.assertEqual(cso.read(), """\ ++---+---+ +| a | b | ++---+---+ +| 1 | 2 | +| 3 | 4 | ++---+---+ +""") + + def test_print_list_with_generator(self): + Row = collections.namedtuple('Row', ['a', 'b']) + + def gen_rows(): + for row in [Row(a=1, b=2), Row(a=3, b=4)]: + yield row + with CaptureStdout() as cso: + utils.print_list(gen_rows(), ['a', 'b']) + self.assertEqual(cso.read(), """\ ++---+---+ +| a | b | ++---+---+ +| 1 | 2 | +| 3 | 4 | ++---+---+ +""") From 7c43330c3fd36b1c95c8dae5d122f9d06b5f6c4a Mon Sep 17 00:00:00 2001 From: Vasyl Khomenko Date: Wed, 15 May 2013 03:24:06 -0700 Subject: [PATCH 04/51] Add .coveragerc file to show correct code coverage Change-Id: I85eaa7f96c9ebf82984356cd08f2d05a0915e0e5 --- .coveragerc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..7d58cb8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = cinderclient +omit = cinderclient/openstack/* + +[report] +ignore-errors = True From 24b4039bae557a8f29be783c3fdd3f9f148c4b0e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 09:09:35 -0700 Subject: [PATCH 05/51] Migrate to flake8. Fixes bug 1172444. Change-Id: Ia063ec67de9e6061ce38b948c9eb60b5589c7bb4 --- run_tests.sh | 22 ++-------------------- tests/v1/fakes.py | 4 ++-- tests/v1/test_auth.py | 2 +- tests/v2/fakes.py | 4 ++-- tests/v2/test_auth.py | 2 +- tools/test-requires | 7 ++++++- tox.ini | 8 ++++++-- 7 files changed, 20 insertions(+), 29 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 9b3684f..ef018f3 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -117,26 +117,8 @@ function copy_subunit_log { } function run_pep8 { - echo "Running pep8 ..." - srcfiles="cinderclient tests" - # Just run PEP8 in current environment - # - # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the - # following reasons: - # - # 1. It's needed to preserve traceback information when re-raising - # exceptions; this is needed b/c Eventlet will clear exceptions when - # switching contexts. - # - # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this - # in Python 2 (in Python 3 `with_traceback` could be used). - # - # 3. Can find no corroborating evidence that this is deprecated in Python 2 - # other than what the PEP8 tool claims. It is deprecated in Python 3, so, - # perhaps the mistake was thinking that the deprecation applied to Python 2 - # as well. - pep8_opts="--ignore=E202,W602 --repeat" - ${wrapper} pep8 ${pep8_opts} ${srcfiles} + echo "Running flake8 ..." + ${wrapper} flake8 } TESTRTESTS="testr run --parallel $testropts" diff --git a/tests/v1/fakes.py b/tests/v1/fakes.py index ee4a58d..b78e8ef 100644 --- a/tests/v1/fakes.py +++ b/tests/v1/fakes.py @@ -302,10 +302,10 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, { 'volume_types': [{'id': 1, 'name': 'test-type-1', - 'extra_specs':{}}, + 'extra_specs': {}}, {'id': 2, 'name': 'test-type-2', - 'extra_specs':{}}]}) + 'extra_specs': {}}]}) def get_types_1(self, **kw): return (200, {}, {'volume_type': {'id': 1, diff --git a/tests/v1/test_auth.py b/tests/v1/test_auth.py index 4ad1231..c0b59e9 100644 --- a/tests/v1/test_auth.py +++ b/tests/v1/test_auth.py @@ -192,7 +192,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): } correct_response = json.dumps(dict_correct_response) dict_responses = [ - {"headers": {'location':'http://127.0.0.1:5001'}, + {"headers": {'location': 'http://127.0.0.1:5001'}, "status_code": 305, "text": "Use proxy"}, # Configured on admin port, cinder redirects to v2.0 port. diff --git a/tests/v2/fakes.py b/tests/v2/fakes.py index 038a434..8567591 100644 --- a/tests/v2/fakes.py +++ b/tests/v2/fakes.py @@ -309,10 +309,10 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, { 'volume_types': [{'id': 1, 'name': 'test-type-1', - 'extra_specs':{}}, + 'extra_specs': {}}, {'id': 2, 'name': 'test-type-2', - 'extra_specs':{}}]}) + 'extra_specs': {}}]}) def get_types_1(self, **kw): return (200, {}, {'volume_type': {'id': 1, diff --git a/tests/v2/test_auth.py b/tests/v2/test_auth.py index b18a7a3..b29752a 100644 --- a/tests/v2/test_auth.py +++ b/tests/v2/test_auth.py @@ -208,7 +208,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): } correct_response = json.dumps(dict_correct_response) dict_responses = [ - {"headers": {'location':'http://127.0.0.1:5001'}, + {"headers": {'location': 'http://127.0.0.1:5001'}, "status_code": 305, "text": "Use proxy"}, # Configured on admin port, cinder redirects to v2.0 port. diff --git a/tools/test-requires b/tools/test-requires index 4dd5249..74be769 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,10 +1,15 @@ distribute>=0.6.24 +# Install bounded pep8/pyflakes first, then let flake8 install +pep8==1.4.5 +pyflakes==0.7.2 +flake8==2.0 +hacking>=0.5.3,<0.6 + coverage discover fixtures mock -pep8==1.3.3 sphinx>=1.1.2 testrepository>=0.0.13 testtools>=0.9.22 diff --git a/tox.ini b/tox.ini index 34ca825..4f37062 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,7 @@ deps = -r{toxinidir}/tools/pip-requires commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -deps = pep8 -commands = pep8 --repeat --show-source cinderclient setup.py +commands = flake8 [testenv:venv] commands = {posargs} @@ -23,3 +22,8 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip + +[flake8] +show-source = True +ignore = F,H +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 7c71fd318ea26befaf05b182854265b9bdd787b5 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Sun, 19 May 2013 18:24:14 +0300 Subject: [PATCH 06/51] Make ManagerWithFind abstract and fix its descendants ManagerWithFind requires list() method in its descendants. Make it abstract and fix its improper descendants that do not implement list() (QuotaSetManager and others). Change-Id: I691ca389b5fea4c1bb36499a264b578fa825bbbf Fixes: bug #1180393 --- cinderclient/base.py | 11 +++++++---- cinderclient/v1/quota_classes.py | 2 +- cinderclient/v1/quotas.py | 2 +- cinderclient/v1/volume_backups_restore.py | 2 +- cinderclient/v2/quota_classes.py | 2 +- cinderclient/v2/quotas.py | 2 +- cinderclient/v2/volume_backups_restore.py | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 1ee621a..6024f59 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -18,7 +18,7 @@ """ Base utilities to build API operation managers and objects on top of. """ - +import abc import contextlib import hashlib import os @@ -167,6 +167,12 @@ class ManagerWithFind(Manager): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def list(self): + pass + def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. @@ -204,9 +210,6 @@ class ManagerWithFind(Manager): return found - def list(self): - raise NotImplementedError - class Resource(object): """ diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index 6aa4fdc..caadd48 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -28,7 +28,7 @@ class QuotaClassSet(base.Resource): self.manager.update(self.class_name, *args, **kwargs) -class QuotaClassSetManager(base.ManagerWithFind): +class QuotaClassSetManager(base.Manager): resource_class = QuotaClassSet def get(self, class_name): diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index 2ac22df..846fb13 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -28,7 +28,7 @@ class QuotaSet(base.Resource): self.manager.update(self.tenant_id, *args, **kwargs) -class QuotaSetManager(base.ManagerWithFind): +class QuotaSetManager(base.Manager): resource_class = QuotaSet def get(self, tenant_id): diff --git a/cinderclient/v1/volume_backups_restore.py b/cinderclient/v1/volume_backups_restore.py index 9405f12..faf9e09 100644 --- a/cinderclient/v1/volume_backups_restore.py +++ b/cinderclient/v1/volume_backups_restore.py @@ -27,7 +27,7 @@ class VolumeBackupsRestore(base.Resource): return "" % self.id -class VolumeBackupRestoreManager(base.ManagerWithFind): +class VolumeBackupRestoreManager(base.Manager): """Manage :class:`VolumeBackupsRestore` resources.""" resource_class = VolumeBackupsRestore diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index d8b3e2f..cc0b753 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -27,7 +27,7 @@ class QuotaClassSet(base.Resource): self.manager.update(self.class_name, *args, **kwargs) -class QuotaClassSetManager(base.ManagerWithFind): +class QuotaClassSetManager(base.Manager): resource_class = QuotaClassSet def get(self, class_name): diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 803d72c..7c9db90 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -27,7 +27,7 @@ class QuotaSet(base.Resource): self.manager.update(self.tenant_id, *args, **kwargs) -class QuotaSetManager(base.ManagerWithFind): +class QuotaSetManager(base.Manager): resource_class = QuotaSet def get(self, tenant_id): diff --git a/cinderclient/v2/volume_backups_restore.py b/cinderclient/v2/volume_backups_restore.py index 9405f12..faf9e09 100644 --- a/cinderclient/v2/volume_backups_restore.py +++ b/cinderclient/v2/volume_backups_restore.py @@ -27,7 +27,7 @@ class VolumeBackupsRestore(base.Resource): return "" % self.id -class VolumeBackupRestoreManager(base.ManagerWithFind): +class VolumeBackupRestoreManager(base.Manager): """Manage :class:`VolumeBackupsRestore` resources.""" resource_class = VolumeBackupsRestore From a2c58b9e6e23f603cd8b4ad9f00c14ada82b110e Mon Sep 17 00:00:00 2001 From: Nikolaj Starodubtsev Date: Thu, 18 Apr 2013 10:07:21 +0400 Subject: [PATCH 07/51] Implement scheduler hints for APIv2 We've done this implementation because we need to use scheduler hint in cinder with some specific filters. So, most part of code have been imported from nova. bp scheduler-hints docimpact Change-Id: Ib7e858e98110a30c5f8d5ff05d03ce2d61f04f92 --- cinderclient/v2/shell.py | 26 ++++++++++++++++++++++++-- cinderclient/v2/volumes.py | 7 +++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 081fe23..dac450b 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -223,6 +223,12 @@ def do_show(cs, args): metavar='', help='Metadata key=value pairs (Optional, Default=None)', default=None) +@utils.arg('--hint', + metavar='', + dest='scheduler_hints', + action='append', + default=[], + help='Scheduler hint like in nova') @utils.service_type('volume') def do_create(cs, args): """Add a new volume.""" @@ -237,6 +243,21 @@ def do_create(cs, args): if args.metadata is not None: volume_metadata = _extract_metadata(args) + #NOTE(N.S.): take this piece from novaclient + hints = {} + if args.scheduler_hints: + for hint in args.scheduler_hints: + key, _sep, value = hint.partition('=') + # NOTE(vish): multiple copies of the same hint will + # result in a list of values + if key in hints: + if isinstance(hints[key], basestring): + hints[key] = [hints[key]] + hints[key] += [value] + else: + hints[key] = value + #NOTE(N.S.): end of the taken piece + volume = cs.volumes.create(args.size, args.snapshot_id, args.source_volid, @@ -245,10 +266,11 @@ def do_create(cs, args): args.volume_type, availability_zone=args.availability_zone, imageRef=args.image_id, - metadata=volume_metadata) + metadata=volume_metadata, + scheduler_hints=hints) info = dict() - volume = cs.volumes.get(info['id']) + volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links') diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index cf9f9ac..5572842 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -105,7 +105,7 @@ class VolumeManager(base.ManagerWithFind): name=None, description=None, volume_type=None, user_id=None, project_id=None, availability_zone=None, - metadata=None, imageRef=None): + metadata=None, imageRef=None, scheduler_hints=None): """Create a volume. :param size: Size of volume in GB @@ -120,7 +120,9 @@ class VolumeManager(base.ManagerWithFind): :param metadata: Optional metadata to set on volume creation :param imageRef: reference to an image stored in glance :param source_volid: ID of source volume to clone from - """ + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + """ if metadata is None: volume_metadata = {} @@ -140,6 +142,7 @@ class VolumeManager(base.ManagerWithFind): 'metadata': volume_metadata, 'imageRef': imageRef, 'source_volid': source_volid, + 'scheduler_hints': scheduler_hints, }} return self._create('/volumes', body, 'volume') From aa2808337b5fd1e585a38b16eff7a495f1ff99f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 09:13:05 -0700 Subject: [PATCH 08/51] Migrate to pbr. Fixes bug 1179007. Change-Id: I66b78ec4b5ba70a1bf1e375a5d1b7575a1879730 --- cinderclient/__init__.py | 6 +- cinderclient/openstack/common/setup.py | 367 ----------------------- cinderclient/openstack/common/version.py | 94 ------ openstack-common.conf | 2 +- setup.cfg | 33 ++ setup.py | 53 +--- tools/pip-requires | 2 + 7 files changed, 47 insertions(+), 510 deletions(-) delete mode 100644 cinderclient/openstack/common/setup.py delete mode 100644 cinderclient/openstack/common/version.py diff --git a/cinderclient/__init__.py b/cinderclient/__init__.py index 6f6043c..5d43513 100644 --- a/cinderclient/__init__.py +++ b/cinderclient/__init__.py @@ -14,9 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient.openstack.common import version +__all__ = ['__version__'] -version_info = version.VersionInfo('python-cinderclient') +import pbr.version + +version_info = pbr.version.VersionInfo('python-cinderclient') # We have a circular import problem when we first run python setup.py sdist # It's harmless, so deflect it. try: diff --git a/cinderclient/openstack/common/setup.py b/cinderclient/openstack/common/setup.py deleted file mode 100644 index ba6b54a..0000000 --- a/cinderclient/openstack/common/setup.py +++ /dev/null @@ -1,367 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# 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. - -""" -Utilities with minimum-depends for use in setup.py -""" - -import email -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - try: - canonical_email, alias = re.match( - r'[^#]*?(<.+>).*(<.+>).*', l).groups() - except AttributeError: - continue - mapping[alias] = canonical_email - return mapping - - -def _parse_git_mailmap(git_dir, mailmap='.mailmap'): - mailmap = os.path.join(os.path.dirname(git_dir), mailmap) - return parse_mailmap(mailmap) - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email_address in mapping.iteritems(): - changelog = changelog.replace(alias, email_address) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def _run_shell_command(cmd, throw_on_error=False): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out = output.communicate() - if output.returncode and throw_on_error: - raise Exception("%s returned %d" % cmd, output.returncode) - if len(out) == 0: - return None - if len(out[0].strip()) == 0: - return None - return out[0].strip() - - -def _get_git_directory(): - parent_dir = os.path.dirname(__file__) - while True: - git_dir = os.path.join(parent_dir, '.git') - if os.path.exists(git_dir): - return git_dir - parent_dir, child = os.path.split(parent_dir) - if not child: # reached to root dir - return None - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - git_dir = _get_git_directory() - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if git_dir: - git_log_cmd = 'git --git-dir=%s log' % git_dir - changelog = _run_shell_command(git_log_cmd) - mailmap = _parse_git_mailmap(git_dir) - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - git_dir = _get_git_directory() - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if git_dir: - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git --git-dir=" + git_dir + - " log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - signed_cmd = ("git --git-dir=" + git_dir + - " log | grep -i Co-authored-by: | sort -u") - signed_entries = _run_shell_command(signed_cmd) - if signed_entries: - new_entries = "\n".join( - [signed.split(":", 1)[1].strip() - for signed in signed_entries.split("\n") if signed]) - changelog = "\n".join((changelog, new_entries)) - mailmap = _parse_git_mailmap(git_dir) - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - - builders = ['html', 'man'] - - def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print "Generating %s" % output_filename - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in self.builders: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - - class LocalBuildLatex(LocalBuildDoc): - builders = ['latex'] - - cmdclass['build_sphinx'] = LocalBuildDoc - cmdclass['build_sphinx_latex'] = LocalBuildLatex - except ImportError: - pass - - return cmdclass - - -def _get_revno(git_dir): - """Return the number of commits since the most recent tag. - - We use git-describe to find this out, but if there are no - tags then we fall back to counting commits since the beginning - of time. - """ - describe = _run_shell_command( - "git --git-dir=%s describe --always" % git_dir) - if "-" in describe: - return describe.rsplit("-", 2)[-2] - - # no tags found - revlist = _run_shell_command( - "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir) - return len(revlist.splitlines()) - - -def _get_version_from_git(pre_version): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - git_dir = _get_git_directory() - if git_dir: - if pre_version: - try: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --exact-match", - throw_on_error=True).replace('-', '.') - except Exception: - sha = _run_shell_command( - "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h") - return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha) - else: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --always").replace( - '-', '.') - return None - - -def _get_version_from_pkg_info(package_name): - """Get the version from PKG-INFO file if we can.""" - try: - pkg_info_file = open('PKG-INFO', 'r') - except (IOError, OSError): - return None - try: - pkg_info = email.message_from_file(pkg_info_file) - except email.MessageError: - return None - # Check to make sure we're in our own dir - if pkg_info.get('Name', None) != package_name: - return None - return pkg_info.get('Version', None) - - -def get_version(package_name, pre_version=None): - """Get the version of the project. First, try getting it from PKG-INFO, if - it exists. If it does, that means we're in a distribution tarball or that - install has happened. Otherwise, if there is no PKG-INFO file, pull the - version from git. - - We do not support setup.py version sanity in git archive tarballs, nor do - we support packagers directly sucking our git repo into theirs. We expect - that a source tarball be made from our git repo - or that if someone wants - to make a source tarball from a fork of our repo with additional tags in it - that they understand and desire the results of doing that. - """ - version = os.environ.get("OSLO_PACKAGE_VERSION", None) - if version: - return version - version = _get_version_from_pkg_info(package_name) - if version: - return version - version = _get_version_from_git(pre_version) - if version: - return version - raise Exception("Versioning for this project requires either an sdist" - " tarball, or access to an upstream git repository.") diff --git a/cinderclient/openstack/common/version.py b/cinderclient/openstack/common/version.py deleted file mode 100644 index c476d19..0000000 --- a/cinderclient/openstack/common/version.py +++ /dev/null @@ -1,94 +0,0 @@ - -# Copyright 2012 OpenStack Foundation -# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# -# 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. - -""" -Utilities for consuming the version from pkg_resources. -""" - -import pkg_resources - - -class VersionInfo(object): - - def __init__(self, package): - """Object that understands versioning for a package - :param package: name of the python package, such as glance, or - python-glanceclient - """ - self.package = package - self.release = None - self.version = None - self._cached_version = None - - def __str__(self): - """Make the VersionInfo object behave like a string.""" - return self.version_string() - - def __repr__(self): - """Include the name.""" - return "VersionInfo(%s:%s)" % (self.package, self.version_string()) - - def _get_version_from_pkg_resources(self): - """Get the version of the package from the pkg_resources record - associated with the package.""" - try: - requirement = pkg_resources.Requirement.parse(self.package) - provider = pkg_resources.get_provider(requirement) - return provider.version - except pkg_resources.DistributionNotFound: - # The most likely cause for this is running tests in a tree - # produced from a tarball where the package itself has not been - # installed into anything. Revert to setup-time logic. - from cinderclient.openstack.common import setup - return setup.get_version(self.package) - - def release_string(self): - """Return the full version of the package including suffixes indicating - VCS status. - """ - if self.release is None: - self.release = self._get_version_from_pkg_resources() - - return self.release - - def version_string(self): - """Return the short version minus any alpha/beta tags.""" - if self.version is None: - parts = [] - for part in self.release_string().split('.'): - if part[0].isdigit(): - parts.append(part) - else: - break - self.version = ".".join(parts) - - return self.version - - # Compatibility functions - canonical_version_string = version_string - version_string_with_vcs = release_string - - def cached_version_string(self, prefix=""): - """Generate an object which will expand in a string context to - the results of version_string(). We do this so that don't - call into pkg_resources every time we start up a program when - passing version information into the CONF constructor, but - rather only do the calculation when and if a version is requested - """ - if not self._cached_version: - self._cached_version = "%s%s" % (prefix, - self.version_string()) - return self._cached_version diff --git a/openstack-common.conf b/openstack-common.conf index 39d114c..35e0ccf 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup,version,strutils +modules=strutils # The base module to hold the copy of openstack.common base=cinderclient diff --git a/setup.cfg b/setup.cfg index 6d6fe64..f332186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,36 @@ +[metadata] +name = python-cinderclient +summary = OpenStack Block Storage API Client Library +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[files] +packages = + cinderclient + +[entry_points] +console_scripts = + cinder = cinderclient.shell:main + [build_sphinx] all_files = 1 source-dir = doc/source diff --git a/setup.py b/setup.py index ffefec4..1e9882d 100644 --- a/setup.py +++ b/setup.py @@ -1,60 +1,21 @@ -# Copyright 2011 OpenStack, LLC +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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 +# 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. +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. # See the License for the specific language governing permissions and # limitations under the License. -import os import setuptools - -from cinderclient.openstack.common import setup - -requires = setup.parse_requirements() -depend_links = setup.parse_dependency_links() -tests_require = setup.parse_requirements(['tools/test-requires']) -project = 'python-cinderclient' - - -def read_file(file_name): - return open(os.path.join(os.path.dirname(__file__), file_name)).read() - - setuptools.setup( - name=project, - version=setup.get_version(project), - author="OpenStack Contributors", - author_email="openstack-dev@lists.openstack.org", - description="Client library for OpenStack Cinder API.", - long_description=read_file("README.rst"), - license="Apache License, Version 2.0", - url="https://github.com/openstack/python-cinderclient", - packages=setuptools.find_packages(exclude=['tests', 'tests.*']), - cmdclass=setup.get_cmdclass(), - install_requires=requires, - tests_require=tests_require, - setup_requires=['setuptools-git>=0.4'], - include_package_data=True, - dependency_links=depend_links, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Environment :: OpenStack", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python" - ], - entry_points={ - "console_scripts": ["cinder = cinderclient.shell:main"] - } -) + setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5,<0.6'], + d2to1=True) diff --git a/tools/pip-requires b/tools/pip-requires index 88b00d2..d677ca2 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,3 +1,5 @@ +d2to1>=0.2.10,<0.3 +pbr>=0.5,<0.6 argparse prettytable>=0.6,<0.8 requests>=0.8 From c82a811301d78ecdabf7786e0c5cee10879db87f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 09:18:07 -0700 Subject: [PATCH 09/51] Rename requires files to standard names. Fixes bug 1179008. Change-Id: I6765bb82df1ae672790662a30ee3527450685036 --- tools/pip-requires => requirements.txt | 0 tools/test-requires => test-requirements.txt | 0 tools/install_venv.py | 4 ++-- tox.ini | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename tools/pip-requires => requirements.txt (100%) rename tools/test-requires => test-requirements.txt (100%) diff --git a/tools/pip-requires b/requirements.txt similarity index 100% rename from tools/pip-requires rename to requirements.txt diff --git a/tools/test-requires b/test-requirements.txt similarity index 100% rename from tools/test-requires rename to test-requirements.txt diff --git a/tools/install_venv.py b/tools/install_venv.py index db0e32b..f22c18d 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -31,8 +31,8 @@ import platform ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') +PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt') +TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt') PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) diff --git a/tox.ini b/tox.ini index 4f37062..b2785e5 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ setenv = VIRTUAL_ENV={envdir} LANGUAGE=en_US:en LC_ALL=C -deps = -r{toxinidir}/tools/pip-requires - -r{toxinidir}/tools/test-requires +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] From bf1ce848e697418bf7cd4a5b955930e0cf41b44a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 09:22:23 -0700 Subject: [PATCH 10/51] Move tests into cinderclient package. tests/__init__.py implies a package in the global namespace called tests. That's not what these are - they're tests in the cinderclient namespace. Change-Id: I29c95bcd8747c3f5f21d5d900879c9b6b1c9a963 --- {tests => cinderclient/tests}/__init__.py | 0 {tests => cinderclient/tests}/fakes.py | 0 {tests => cinderclient/tests}/test_base.py | 4 ++-- {tests => cinderclient/tests}/test_client.py | 2 +- {tests => cinderclient/tests}/test_http.py | 2 +- {tests => cinderclient/tests}/test_service_catalog.py | 2 +- {tests => cinderclient/tests}/test_shell.py | 2 +- {tests => cinderclient/tests}/test_utils.py | 2 +- {tests => cinderclient/tests}/utils.py | 0 {tests => cinderclient/tests}/v1/__init__.py | 0 {tests => cinderclient/tests}/v1/contrib/__init__.py | 0 .../tests}/v1/contrib/test_list_extensions.py | 4 ++-- {tests => cinderclient/tests}/v1/fakes.py | 4 ++-- {tests => cinderclient/tests}/v1/test_auth.py | 2 +- {tests => cinderclient/tests}/v1/test_quota_classes.py | 4 ++-- {tests => cinderclient/tests}/v1/test_quotas.py | 4 ++-- {tests => cinderclient/tests}/v1/test_shell.py | 4 ++-- {tests => cinderclient/tests}/v1/test_types.py | 4 ++-- {tests => cinderclient/tests}/v1/test_volume_backups.py | 4 ++-- {tests => cinderclient/tests}/v1/test_volumes.py | 4 ++-- {tests => cinderclient/tests}/v1/testfile.txt | 0 {tests => cinderclient/tests}/v2/__init__.py | 0 {tests => cinderclient/tests}/v2/contrib/__init__.py | 0 .../tests}/v2/contrib/test_list_extensions.py | 4 ++-- {tests => cinderclient/tests}/v2/fakes.py | 4 ++-- {tests => cinderclient/tests}/v2/test_auth.py | 2 +- {tests => cinderclient/tests}/v2/test_quota_classes.py | 4 ++-- {tests => cinderclient/tests}/v2/test_quotas.py | 4 ++-- {tests => cinderclient/tests}/v2/test_shell.py | 4 ++-- {tests => cinderclient/tests}/v2/test_types.py | 4 ++-- {tests => cinderclient/tests}/v2/test_volume_backups.py | 4 ++-- {tests => cinderclient/tests}/v2/test_volumes.py | 4 ++-- 32 files changed, 41 insertions(+), 41 deletions(-) rename {tests => cinderclient/tests}/__init__.py (100%) rename {tests => cinderclient/tests}/fakes.py (100%) rename {tests => cinderclient/tests}/test_base.py (95%) rename {tests => cinderclient/tests}/test_client.py (94%) rename {tests => cinderclient/tests}/test_http.py (99%) rename {tests => cinderclient/tests}/test_service_catalog.py (99%) rename {tests => cinderclient/tests}/test_shell.py (98%) rename {tests => cinderclient/tests}/test_utils.py (98%) rename {tests => cinderclient/tests}/utils.py (100%) rename {tests => cinderclient/tests}/v1/__init__.py (100%) rename {tests => cinderclient/tests}/v1/contrib/__init__.py (100%) rename {tests => cinderclient/tests}/v1/contrib/test_list_extensions.py (87%) rename {tests => cinderclient/tests}/v1/fakes.py (99%) rename {tests => cinderclient/tests}/v1/test_auth.py (99%) rename {tests => cinderclient/tests}/v1/test_quota_classes.py (94%) rename {tests => cinderclient/tests}/v1/test_quotas.py (95%) rename {tests => cinderclient/tests}/v1/test_shell.py (98%) rename {tests => cinderclient/tests}/v1/test_types.py (92%) rename {tests => cinderclient/tests}/v1/test_volume_backups.py (95%) rename {tests => cinderclient/tests}/v1/test_volumes.py (96%) rename {tests => cinderclient/tests}/v1/testfile.txt (100%) rename {tests => cinderclient/tests}/v2/__init__.py (100%) rename {tests => cinderclient/tests}/v2/contrib/__init__.py (100%) rename {tests => cinderclient/tests}/v2/contrib/test_list_extensions.py (93%) rename {tests => cinderclient/tests}/v2/fakes.py (99%) rename {tests => cinderclient/tests}/v2/test_auth.py (99%) rename {tests => cinderclient/tests}/v2/test_quota_classes.py (94%) rename {tests => cinderclient/tests}/v2/test_quotas.py (95%) rename {tests => cinderclient/tests}/v2/test_shell.py (98%) rename {tests => cinderclient/tests}/v2/test_types.py (95%) rename {tests => cinderclient/tests}/v2/test_volume_backups.py (95%) rename {tests => cinderclient/tests}/v2/test_volumes.py (97%) diff --git a/tests/__init__.py b/cinderclient/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to cinderclient/tests/__init__.py diff --git a/tests/fakes.py b/cinderclient/tests/fakes.py similarity index 100% rename from tests/fakes.py rename to cinderclient/tests/fakes.py diff --git a/tests/test_base.py b/cinderclient/tests/test_base.py similarity index 95% rename from tests/test_base.py rename to cinderclient/tests/test_base.py index 7eba986..75c37e6 100644 --- a/tests/test_base.py +++ b/cinderclient/tests/test_base.py @@ -1,8 +1,8 @@ from cinderclient import base from cinderclient import exceptions from cinderclient.v1 import volumes -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/test_client.py b/cinderclient/tests/test_client.py similarity index 94% rename from tests/test_client.py rename to cinderclient/tests/test_client.py index 1b72971..17b2b88 100644 --- a/tests/test_client.py +++ b/cinderclient/tests/test_client.py @@ -2,7 +2,7 @@ import cinderclient.client import cinderclient.v1.client import cinderclient.v2.client -from tests import utils +from cinderclient.tests import utils class ClientTest(utils.TestCase): diff --git a/tests/test_http.py b/cinderclient/tests/test_http.py similarity index 99% rename from tests/test_http.py rename to cinderclient/tests/test_http.py index cfc1885..3b93a4b 100644 --- a/tests/test_http.py +++ b/cinderclient/tests/test_http.py @@ -4,7 +4,7 @@ import requests from cinderclient import client from cinderclient import exceptions -from tests import utils +from cinderclient.tests import utils fake_response = utils.TestResponse({ diff --git a/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py similarity index 99% rename from tests/test_service_catalog.py rename to cinderclient/tests/test_service_catalog.py index 8dabd99..c9d9819 100644 --- a/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -1,6 +1,6 @@ from cinderclient import exceptions from cinderclient import service_catalog -from tests import utils +from cinderclient.tests import utils # Taken directly from keystone/content/common/samples/auth.json diff --git a/tests/test_shell.py b/cinderclient/tests/test_shell.py similarity index 98% rename from tests/test_shell.py rename to cinderclient/tests/test_shell.py index 580dd25..8ebe0f7 100644 --- a/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -8,7 +8,7 @@ from testtools import matchers from cinderclient import exceptions import cinderclient.shell -from tests import utils +from cinderclient.tests import utils class ShellTest(utils.TestCase): diff --git a/tests/test_utils.py b/cinderclient/tests/test_utils.py similarity index 98% rename from tests/test_utils.py rename to cinderclient/tests/test_utils.py index af3f2dd..95b50d0 100644 --- a/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -5,7 +5,7 @@ import sys from cinderclient import exceptions from cinderclient import utils from cinderclient import base -from tests import utils as test_utils +from cinderclient.tests import utils as test_utils UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' diff --git a/tests/utils.py b/cinderclient/tests/utils.py similarity index 100% rename from tests/utils.py rename to cinderclient/tests/utils.py diff --git a/tests/v1/__init__.py b/cinderclient/tests/v1/__init__.py similarity index 100% rename from tests/v1/__init__.py rename to cinderclient/tests/v1/__init__.py diff --git a/tests/v1/contrib/__init__.py b/cinderclient/tests/v1/contrib/__init__.py similarity index 100% rename from tests/v1/contrib/__init__.py rename to cinderclient/tests/v1/contrib/__init__.py diff --git a/tests/v1/contrib/test_list_extensions.py b/cinderclient/tests/v1/contrib/test_list_extensions.py similarity index 87% rename from tests/v1/contrib/test_list_extensions.py rename to cinderclient/tests/v1/contrib/test_list_extensions.py index faf10bb..8066c54 100644 --- a/tests/v1/contrib/test_list_extensions.py +++ b/cinderclient/tests/v1/contrib/test_list_extensions.py @@ -1,8 +1,8 @@ from cinderclient import extension from cinderclient.v1.contrib import list_extensions -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes extensions = [ diff --git a/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py similarity index 99% rename from tests/v1/fakes.py rename to cinderclient/tests/v1/fakes.py index b78e8ef..411c5e1 100644 --- a/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -16,9 +16,9 @@ import urlparse from cinderclient import client as base_client +from cinderclient.tests import fakes +import cinderclient.tests.utils as utils from cinderclient.v1 import client -from tests import fakes -import tests.utils as utils def _stub_volume(**kwargs): diff --git a/tests/v1/test_auth.py b/cinderclient/tests/v1/test_auth.py similarity index 99% rename from tests/v1/test_auth.py rename to cinderclient/tests/v1/test_auth.py index c0b59e9..704eacc 100644 --- a/tests/v1/test_auth.py +++ b/cinderclient/tests/v1/test_auth.py @@ -5,7 +5,7 @@ import requests from cinderclient.v1 import client from cinderclient import exceptions -from tests import utils +from cinderclient.tests import utils class AuthenticateAgainstKeystoneTests(utils.TestCase): diff --git a/tests/v1/test_quota_classes.py b/cinderclient/tests/v1/test_quota_classes.py similarity index 94% rename from tests/v1/test_quota_classes.py rename to cinderclient/tests/v1/test_quota_classes.py index 4d4cb58..33b390d 100644 --- a/tests/v1/test_quota_classes.py +++ b/cinderclient/tests/v1/test_quota_classes.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1/test_quotas.py b/cinderclient/tests/v1/test_quotas.py similarity index 95% rename from tests/v1/test_quotas.py rename to cinderclient/tests/v1/test_quotas.py index 7afc626..7ebb061 100644 --- a/tests/v1/test_quotas.py +++ b/cinderclient/tests/v1/test_quotas.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py similarity index 98% rename from tests/v1/test_shell.py rename to cinderclient/tests/v1/test_shell.py index 8d28309..9e8e2f7 100644 --- a/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -22,8 +22,8 @@ import fixtures from cinderclient import client from cinderclient import shell from cinderclient.v1 import shell as shell_v1 -from tests.v1 import fakes -from tests import utils +from cinderclient.tests.v1 import fakes +from cinderclient.tests import utils class ShellTest(utils.TestCase): diff --git a/tests/v1/test_types.py b/cinderclient/tests/v1/test_types.py similarity index 92% rename from tests/v1/test_types.py rename to cinderclient/tests/v1/test_types.py index 92aa2c0..41a89b7 100644 --- a/tests/v1/test_types.py +++ b/cinderclient/tests/v1/test_types.py @@ -1,7 +1,7 @@ from cinderclient import exceptions from cinderclient.v1 import volume_types -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1/test_volume_backups.py b/cinderclient/tests/v1/test_volume_backups.py similarity index 95% rename from tests/v1/test_volume_backups.py rename to cinderclient/tests/v1/test_volume_backups.py index 4c11fce..dd7ec0d 100644 --- a/tests/v1/test_volume_backups.py +++ b/cinderclient/tests/v1/test_volume_backups.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py similarity index 96% rename from tests/v1/test_volumes.py rename to cinderclient/tests/v1/test_volumes.py index 3306fdb..16410c9 100644 --- a/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -1,5 +1,5 @@ -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1/testfile.txt b/cinderclient/tests/v1/testfile.txt similarity index 100% rename from tests/v1/testfile.txt rename to cinderclient/tests/v1/testfile.txt diff --git a/tests/v2/__init__.py b/cinderclient/tests/v2/__init__.py similarity index 100% rename from tests/v2/__init__.py rename to cinderclient/tests/v2/__init__.py diff --git a/tests/v2/contrib/__init__.py b/cinderclient/tests/v2/contrib/__init__.py similarity index 100% rename from tests/v2/contrib/__init__.py rename to cinderclient/tests/v2/contrib/__init__.py diff --git a/tests/v2/contrib/test_list_extensions.py b/cinderclient/tests/v2/contrib/test_list_extensions.py similarity index 93% rename from tests/v2/contrib/test_list_extensions.py rename to cinderclient/tests/v2/contrib/test_list_extensions.py index 49b0e57..ff59cd2 100644 --- a/tests/v2/contrib/test_list_extensions.py +++ b/cinderclient/tests/v2/contrib/test_list_extensions.py @@ -16,8 +16,8 @@ from cinderclient import extension from cinderclient.v2.contrib import list_extensions -from tests import utils -from tests.v1 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes extensions = [ diff --git a/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py similarity index 99% rename from tests/v2/fakes.py rename to cinderclient/tests/v2/fakes.py index 8567591..f90ace3 100644 --- a/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -15,9 +15,9 @@ import urlparse from cinderclient import client as base_client +from cinderclient.tests import fakes +import cinderclient.tests.utils as utils from cinderclient.v2 import client -from tests import fakes -import tests.utils as utils def _stub_volume(**kwargs): diff --git a/tests/v2/test_auth.py b/cinderclient/tests/v2/test_auth.py similarity index 99% rename from tests/v2/test_auth.py rename to cinderclient/tests/v2/test_auth.py index b29752a..89dd18f 100644 --- a/tests/v2/test_auth.py +++ b/cinderclient/tests/v2/test_auth.py @@ -21,7 +21,7 @@ import requests from cinderclient import exceptions from cinderclient.v2 import client -from tests import utils +from cinderclient.tests import utils class AuthenticateAgainstKeystoneTests(utils.TestCase): diff --git a/tests/v2/test_quota_classes.py b/cinderclient/tests/v2/test_quota_classes.py similarity index 94% rename from tests/v2/test_quota_classes.py rename to cinderclient/tests/v2/test_quota_classes.py index 6be6cc0..ce7646c 100644 --- a/tests/v2/test_quota_classes.py +++ b/cinderclient/tests/v2/test_quota_classes.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes cs = fakes.FakeClient() diff --git a/tests/v2/test_quotas.py b/cinderclient/tests/v2/test_quotas.py similarity index 95% rename from tests/v2/test_quotas.py rename to cinderclient/tests/v2/test_quotas.py index d222e83..37ceeed 100644 --- a/tests/v2/test_quotas.py +++ b/cinderclient/tests/v2/test_quotas.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes cs = fakes.FakeClient() diff --git a/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py similarity index 98% rename from tests/v2/test_shell.py rename to cinderclient/tests/v2/test_shell.py index ce6646f..8f6e074 100644 --- a/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -17,8 +17,8 @@ import fixtures from cinderclient import client from cinderclient import shell -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes class ShellTest(utils.TestCase): diff --git a/tests/v2/test_types.py b/cinderclient/tests/v2/test_types.py similarity index 95% rename from tests/v2/test_types.py rename to cinderclient/tests/v2/test_types.py index f36b768..de8c743 100644 --- a/tests/v2/test_types.py +++ b/cinderclient/tests/v2/test_types.py @@ -15,8 +15,8 @@ # under the License. from cinderclient.v2 import volume_types -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes cs = fakes.FakeClient() diff --git a/tests/v2/test_volume_backups.py b/cinderclient/tests/v2/test_volume_backups.py similarity index 95% rename from tests/v2/test_volume_backups.py rename to cinderclient/tests/v2/test_volume_backups.py index 083da31..44b1c54 100644 --- a/tests/v2/test_volume_backups.py +++ b/cinderclient/tests/v2/test_volume_backups.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes cs = fakes.FakeClient() diff --git a/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py similarity index 97% rename from tests/v2/test_volumes.py rename to cinderclient/tests/v2/test_volumes.py index 8e46da6..a66dd8c 100644 --- a/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -14,8 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v2 import fakes +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes cs = fakes.FakeClient() From 2a446c5b6bf0fa76c7e569e159362ec0d4cf2e6c Mon Sep 17 00:00:00 2001 From: Nicolas Simonds Date: Tue, 21 May 2013 16:19:38 -0700 Subject: [PATCH 11/51] Only add logging handlers if there currently aren't any This corrects an odd problem where Horizon would stand up multiple client objects, which would cause duplicate/triplicate/dozens of repeated log lines in its log files, due to multiple identical handlers being added to the logging object Fixes Bug 1182678 Change-Id: I198f3ecbb687bff69a06a166574b998cce54f2ac --- cinderclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 8e0d323..0cdb861 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -82,7 +82,7 @@ class HTTPClient(object): self.verify_cert = True self._logger = logging.getLogger(__name__) - if self.http_log_debug: + if self.http_log_debug and not self._logger.handlers: ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) From bde6efb65c8a7b2e0bc08e6d9f187e5654cfba22 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Wed, 22 May 2013 14:03:50 -0400 Subject: [PATCH 12/51] Set the correct location for the tests. Change-Id: I4d6247319d393809b65d05ebbd10620fe224a281 --- .testr.conf | 2 +- run_tests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.testr.conf b/.testr.conf index 2109af6..f5be871 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./cinderclient/tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/run_tests.sh b/run_tests.sh index ef018f3..bbee9fc 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -79,7 +79,7 @@ function run_tests { if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then # Default to running all tests if specific test is not # provided. - testrargs="discover ./tests" + testrargs="discover ./cinderclient/tests" fi ${wrapper} python -m testtools.run $testropts $testrargs From 2e58e73a0cdfae43ca206abcc1de0743324d8b68 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Fri, 31 May 2013 13:42:17 -0600 Subject: [PATCH 13/51] Update run_tests and bring back colorizer. This patch adds output of tests and their results to run_tests.sh. It also brings back colorizer to the output and updates the test-requirements. Should align with cinder changes that are in progress at: https://review.openstack.org/#/c/30291/ Change-Id: I3df6d861f4b4d4355464ceb2d507e69bcf682fbe --- run_tests.sh | 148 +++++++++++++------ test-requirements.txt | 10 +- tools/colorizer.py | 335 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 445 insertions(+), 48 deletions(-) create mode 100755 tools/colorizer.py diff --git a/run_tests.sh b/run_tests.sh index bbee9fc..3299a30 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -4,17 +4,27 @@ set -eu function usage { echo "Usage: $0 [OPTION]..." - echo "Run python-cinderclient test suite" + echo "Run cinderclient's test suite(s)" echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" - echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" + echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)." + echo " -n, --no-recreate-db Don't recreate the test database." + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " -u, --update Update the virtual environment with any newer package versions" + echo " -p, --pep8 Just run PEP8 and HACKING compliance check" + echo " -P, --no-pep8 Don't run static code checks" + echo " -c, --coverage Generate coverage report" + echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger." + echo " -h, --help Print this usage message" + echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" + echo " --virtual-env-path Location of the virtualenv directory" + echo " Default: \$(pwd)" + echo " --virtual-env-name Name of the virtualenv directory" + echo " Default: .venv" + echo " --tools-path Location of the tools directory" + echo " Default: \$(pwd)" echo "" echo "Note: with no options specified, the script will try to run the tests in a virtual environment," echo " If no virtualenv is found, the script will ask if you would like to create one. If you " @@ -22,23 +32,44 @@ function usage { exit } -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -s|--no-site-packages) no_site_packages=1;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -d|--debug) debug=1;; - -*) testropts="$testropts $1";; - *) testrargs="$testrargs $1" - esac +function process_options { + i=1 + while [ $i -le $# ]; do + case "${!i}" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -s|--no-site-packages) no_site_packages=1;; + -r|--recreate-db) recreate_db=1;; + -n|--no-recreate-db) recreate_db=0;; + -f|--force) force=1;; + -u|--update) update=1;; + -p|--pep8) just_pep8=1;; + -P|--no-pep8) no_pep8=1;; + -c|--coverage) coverage=1;; + -d|--debug) debug=1;; + --virtual-env-path) + (( i++ )) + venv_path=${!i} + ;; + --virtual-env-name) + (( i++ )) + venv_dir=${!i} + ;; + --tools-path) + (( i++ )) + tools_path=${!i} + ;; + -*) testropts="$testropts ${!i}";; + *) testrargs="$testrargs ${!i}" + esac + (( i++ )) + done } -venv=.venv +tool_path=${tools_path:-$(pwd)} +venv_path=${venv_path:-$(pwd)} +venv_dir=${venv_name:-.venv} with_venv=tools/with_venv.sh always_venv=0 never_venv=0 @@ -52,14 +83,20 @@ just_pep8=0 no_pep8=0 coverage=0 debug=0 +recreate_db=1 +update=0 LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C -for arg in "$@"; do - process_option $arg -done +process_options $@ +# Make our paths available to other scripts we call +export venv_path +export venv_dir +export venv_name +export tools_dir +export venv=${venv_path}/${venv_dir} if [ $no_site_packages -eq 1 ]; then installvenvopts="--no-site-packages" @@ -90,22 +127,40 @@ function run_tests { fi if [ $coverage -eq 1 ]; then - # Do not test test_coverage_ext when gathering coverage. - if [ "x$testrargs" = "x" ]; then - testrargs="^(?!.*test_coverage_ext).*$" - fi - export PYTHON="${wrapper} coverage run --source cinderclient --parallel-mode" + TESTRTESTS="$TESTRTESTS --coverage" + else + TESTRTESTS="$TESTRTESTS" fi + # Just run the test suites in current environment set +e - TESTRTESTS="$TESTRTESTS $testrargs" + testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'` + TESTRTESTS="$TESTRTESTS --testr-args='--subunit $testropts $testrargs'" + if [ setup.cfg -nt cinderclient.egg-info/entry_points.txt ] + then + ${wrapper} python setup.py egg_info + fi echo "Running \`${wrapper} $TESTRTESTS\`" - ${wrapper} $TESTRTESTS + if ${wrapper} which subunit-2to1 2>&1 > /dev/null + then + # subunit-2to1 is present, testr subunit stream should be in version 2 + # format. Convert to version one before colorizing. + bash -c "${wrapper} $TESTRTESTS | ${wrapper} subunit-2to1 | ${wrapper} tools/colorizer.py" + else + bash -c "${wrapper} $TESTRTESTS | ${wrapper} tools/colorizer.py" + fi RESULT=$? set -e copy_subunit_log + if [ $coverage -eq 1 ]; then + echo "Generating coverage report in covhtml/" + # Don't compute coverage for common code, which is tested elsewhere + ${wrapper} coverage combine + ${wrapper} coverage html --include='cinderclient/*' --omit='cinderclient/openstack/common/*' -d covhtml -i + fi + return $RESULT } @@ -118,10 +173,11 @@ function copy_subunit_log { function run_pep8 { echo "Running flake8 ..." - ${wrapper} flake8 + bash -c "${wrapper} flake8" } -TESTRTESTS="testr run --parallel $testropts" + +TESTRTESTS="python setup.py testr" if [ $never_venv -eq 0 ] then @@ -130,6 +186,10 @@ then echo "Cleaning virtualenv..." rm -rf ${venv} fi + if [ $update -eq 1 ]; then + echo "Updating virtualenv..." + python tools/install_venv.py $installvenvopts + fi if [ -e ${venv} ]; then wrapper="${with_venv}" else @@ -159,19 +219,19 @@ if [ $just_pep8 -eq 1 ]; then exit fi +if [ $recreate_db -eq 1 ]; then + rm -f tests.sqlite +fi + init_testr run_tests # NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. +# not when we're running tests individually. To handle this, we need to +# distinguish between options (testropts), which begin with a '-', and +# arguments (testrargs). if [ -z "$testrargs" ]; then if [ $no_pep8 -eq 0 ]; then run_pep8 fi fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage combine - ${wrapper} coverage html --include='cinderclient/*' --omit='cinderclient/openstack/common/*' -d covhtml -i -fi diff --git a/test-requirements.txt b/test-requirements.txt index 74be769..f8dbf56 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,10 +6,12 @@ pyflakes==0.7.2 flake8==2.0 hacking>=0.5.3,<0.6 -coverage +coverage>=3.6 discover -fixtures -mock +fixtures>=0.3.12 +mock>=0.8.0 +mox>=0.5.3 +python-subunit sphinx>=1.1.2 +testtools>=0.9.29 testrepository>=0.0.13 -testtools>=0.9.22 diff --git a/tools/colorizer.py b/tools/colorizer.py new file mode 100755 index 0000000..a49abb1 --- /dev/null +++ b/tools/colorizer.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013, Nebula, Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. +# +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Display a subunit stream through a colorized unittest test runner.""" + +import heapq +import subunit +import sys +import unittest + +import testtools + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except Exception: + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + import win32console + red, green, blue, bold = (win32console.FOREGROUND_RED, + win32console.FOREGROUND_GREEN, + win32console.FOREGROUND_BLUE, + win32console.FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold + } + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +def get_elapsed_time_color(elapsed_time): + if elapsed_time > 1.0: + return 'red' + elif elapsed_time > 0.25: + return 'yellow' + else: + return 'green' + + +class NovaTestResult(testtools.TestResult): + def __init__(self, stream, descriptions, verbosity): + super(NovaTestResult, self).__init__() + self.stream = stream + self.showAll = verbosity > 1 + self.num_slow_tests = 10 + self.slow_tests = [] # this is a fixed-sized heap + self.colorizer = None + # NOTE(vish): reset stdout for the terminal check + stdout = sys.stdout + sys.stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + self.start_time = None + self.last_time = {} + self.results = {} + self.last_written = None + + def _writeElapsedTime(self, elapsed): + color = get_elapsed_time_color(elapsed) + self.colorizer.write(" %.2f" % elapsed, color) + + def _addResult(self, test, *args): + try: + name = test.id() + except AttributeError: + name = 'Unknown.unknown' + test_class, test_name = name.rsplit('.', 1) + + elapsed = (self._now() - self.start_time).total_seconds() + item = (elapsed, test_class, test_name) + if len(self.slow_tests) >= self.num_slow_tests: + heapq.heappushpop(self.slow_tests, item) + else: + heapq.heappush(self.slow_tests, item) + + self.results.setdefault(test_class, []) + self.results[test_class].append((test_name, elapsed) + args) + self.last_time[test_class] = self._now() + self.writeTests() + + def _writeResult(self, test_name, elapsed, long_result, color, + short_result, success): + if self.showAll: + self.stream.write(' %s' % str(test_name).ljust(66)) + self.colorizer.write(long_result, color) + if success: + self._writeElapsedTime(elapsed) + self.stream.writeln() + else: + self.colorizer.write(short_result, color) + + def addSuccess(self, test): + super(NovaTestResult, self).addSuccess(test) + self._addResult(test, 'OK', 'green', '.', True) + + def addFailure(self, test, err): + if test.id() == 'process-returncode': + return + super(NovaTestResult, self).addFailure(test, err) + self._addResult(test, 'FAIL', 'red', 'F', False) + + def addError(self, test, err): + super(NovaTestResult, self).addFailure(test, err) + self._addResult(test, 'ERROR', 'red', 'E', False) + + def addSkip(self, test, reason=None, details=None): + super(NovaTestResult, self).addSkip(test, reason, details) + self._addResult(test, 'SKIP', 'blue', 'S', True) + + def startTest(self, test): + self.start_time = self._now() + super(NovaTestResult, self).startTest(test) + + def writeTestCase(self, cls): + if not self.results.get(cls): + return + if cls != self.last_written: + self.colorizer.write(cls, 'white') + self.stream.writeln() + for result in self.results[cls]: + self._writeResult(*result) + del self.results[cls] + self.stream.flush() + self.last_written = cls + + def writeTests(self): + time = self.last_time.get(self.last_written, self._now()) + if not self.last_written or (self._now() - time).total_seconds() > 2.0: + diff = 3.0 + while diff > 2.0: + classes = self.results.keys() + oldest = min(classes, key=lambda x: self.last_time[x]) + diff = (self._now() - self.last_time[oldest]).total_seconds() + self.writeTestCase(oldest) + else: + self.writeTestCase(self.last_written) + + def done(self): + self.stopTestRun() + + def stopTestRun(self): + for cls in list(self.results.iterkeys()): + self.writeTestCase(cls) + self.stream.writeln() + self.writeSlowTests() + + def writeSlowTests(self): + # Pare out 'fast' tests + slow_tests = [item for item in self.slow_tests + if get_elapsed_time_color(item[0]) != 'green'] + if slow_tests: + slow_total_time = sum(item[0] for item in slow_tests) + slow = ("Slowest %i tests took %.2f secs:" + % (len(slow_tests), slow_total_time)) + self.colorizer.write(slow, 'yellow') + self.stream.writeln() + last_cls = None + # sort by name + for elapsed, cls, name in sorted(slow_tests, + key=lambda x: x[1] + x[2]): + if cls != last_cls: + self.colorizer.write(cls, 'white') + self.stream.writeln() + last_cls = cls + self.stream.write(' %s' % str(name).ljust(68)) + self._writeElapsedTime(elapsed) + self.stream.writeln() + + def printErrors(self): + if self.showAll: + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavor, errors): + for test, err in errors: + self.colorizer.write("=" * 70, 'red') + self.stream.writeln() + self.colorizer.write(flavor, 'red') + self.stream.writeln(": %s" % test.id()) + self.colorizer.write("-" * 70, 'red') + self.stream.writeln() + self.stream.writeln("%s" % err) + + +test = subunit.ProtocolTestCase(sys.stdin, passthrough=None) + +if sys.version_info[0:2] <= (2, 6): + runner = unittest.TextTestRunner(verbosity=2) +else: + runner = unittest.TextTestRunner(verbosity=2, resultclass=NovaTestResult) + +if runner.run(test).wasSuccessful(): + exit_code = 0 +else: + exit_code = 1 +sys.exit(exit_code) From 035ba87c4a6ed491285fd0e232fd0c1153542a48 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 1 Jun 2013 19:56:24 -0500 Subject: [PATCH 14/51] python3: Introduce py33 to tox.ini Introduce py33 to tox.ini to make testing with python3 easier. Change-Id: If979800c5c337996bfdfa5bc99d03ad945192816 Signed-off-by: Chuck Short --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b2785e5..161a11b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,pep8 +envlist = py26,py27,py33,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} From 919db02848e9ee8dd0af4356420adcec62e0b46f Mon Sep 17 00:00:00 2001 From: liyingjun Date: Fri, 7 Jun 2013 11:29:10 +0800 Subject: [PATCH 15/51] Add `snapshots` key support for quota class update Fix bug 1188452 Change-Id: I3db0f3f1191a24571de9631786889ee81af777f6 --- cinderclient/tests/v1/test_quota_classes.py | 2 +- cinderclient/tests/v2/test_quota_classes.py | 2 +- cinderclient/v1/quota_classes.py | 2 ++ cinderclient/v2/quota_classes.py | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/test_quota_classes.py b/cinderclient/tests/v1/test_quota_classes.py index 33b390d..83e297f 100644 --- a/cinderclient/tests/v1/test_quota_classes.py +++ b/cinderclient/tests/v1/test_quota_classes.py @@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase): def test_update_quota(self): q = cs.quota_classes.get('test') - q.update(volumes=2) + q.update(volumes=2, snapshots=2) cs.assert_called('PUT', '/os-quota-class-sets/test') def test_refresh_quota(self): diff --git a/cinderclient/tests/v2/test_quota_classes.py b/cinderclient/tests/v2/test_quota_classes.py index ce7646c..83cc710 100644 --- a/cinderclient/tests/v2/test_quota_classes.py +++ b/cinderclient/tests/v2/test_quota_classes.py @@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase): def test_update_quota(self): q = cs.quota_classes.get('test') - q.update(volumes=2) + q.update(volumes=2, snapshots=2) cs.assert_called('PUT', '/os-quota-class-sets/test') def test_refresh_quota(self): diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index caadd48..f01ff1d 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -38,11 +38,13 @@ class QuotaClassSetManager(base.Manager): def update(self, class_name, volumes=None, + snapshots=None, gigabytes=None): body = {'quota_class_set': { 'class_name': class_name, 'volumes': volumes, + 'snapshots': snapshots, 'gigabytes': gigabytes}} for key in body['quota_class_set'].keys(): diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index cc0b753..c4ab0c5 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -37,11 +37,13 @@ class QuotaClassSetManager(base.Manager): def update(self, class_name, volumes=None, + snapshots=None, gigabytes=None): body = {'quota_class_set': { 'class_name': class_name, 'volumes': volumes, + 'snapshots': snapshots, 'gigabytes': gigabytes}} for key in body['quota_class_set'].keys(): From 7359c976d1692c63118fd8b67ff215c9498725e7 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Sun, 9 Jun 2013 11:18:02 +0200 Subject: [PATCH 16/51] Start Gating on Pyflakes and Hacking Instead of globally ignoring Pyflakes and Hacking warnings, only blacklist those that occur frequently and fix the others. Start gating on those checks. Change-Id: Ice032c16d445ef08ef018bcdc5c221ab3c323755 --- cinderclient/base.py | 2 +- cinderclient/client.py | 3 ++- cinderclient/exceptions.py | 6 ++++-- cinderclient/service_catalog.py | 5 +++-- cinderclient/tests/test_shell.py | 1 - cinderclient/tests/test_utils.py | 2 +- cinderclient/tests/utils.py | 5 +++-- cinderclient/tests/v1/test_shell.py | 2 -- cinderclient/tests/v1/test_types.py | 1 - cinderclient/v1/__init__.py | 2 +- cinderclient/v1/limits.py | 8 ++++---- cinderclient/v1/quota_classes.py | 3 ++- cinderclient/v1/quotas.py | 5 +++-- cinderclient/v1/shell.py | 6 +++--- cinderclient/v2/__init__.py | 2 +- cinderclient/v2/limits.py | 8 ++++---- cinderclient/v2/quota_classes.py | 2 +- cinderclient/v2/quotas.py | 2 +- cinderclient/v2/shell.py | 6 +++--- tox.ini | 2 +- 20 files changed, 38 insertions(+), 35 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 6024f59..1822dfb 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -104,7 +104,7 @@ class Manager(utils.HookableMixin): cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) try: - os.makedirs(cache_dir, 0755) + os.makedirs(cache_dir, 0o755) except OSError: # NOTE(kiall): This is typicaly either permission denied while # attempting to create the directory, or the directory diff --git a/cinderclient/client.py b/cinderclient/client.py index 0cdb861..755ffcb 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -207,7 +207,8 @@ class HTTPClient(object): def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get - back a service catalog with a token and our endpoints.""" + back a service catalog with a token and our endpoints. + """ if resp.status_code == 200: # content must always present try: diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index d7be180..d381246 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -6,7 +6,8 @@ Exception definitions. class UnsupportedVersion(Exception): """Indicates that the user is trying to use an unsupported - version of the API""" + version of the API. + """ pass @@ -24,7 +25,8 @@ class NoUniqueMatch(Exception): class NoTokenLookupException(Exception): """This form of authentication does not support looking up - endpoints from an existing token.""" + endpoints from an existing token. + """ pass diff --git a/cinderclient/service_catalog.py b/cinderclient/service_catalog.py index e1778db..2be335a 100644 --- a/cinderclient/service_catalog.py +++ b/cinderclient/service_catalog.py @@ -33,7 +33,8 @@ class ServiceCatalog(object): service_name=None, volume_service_name=None): """Fetch the public URL from the Compute service for a particular endpoint attribute. If none given, return - the first. See tests for sample service catalog.""" + the first. See tests for sample service catalog. + """ matching_endpoints = [] if 'endpoints' in self.catalog: # We have a bastardized service catalog. Treat it special. :/ @@ -44,7 +45,7 @@ class ServiceCatalog(object): raise cinderclient.exceptions.EndpointNotFound() # We don't always get a service catalog back ... - if not 'serviceCatalog' in self.catalog['access']: + if 'serviceCatalog' not in self.catalog['access']: return None # Full catalog ... diff --git a/cinderclient/tests/test_shell.py b/cinderclient/tests/test_shell.py index 8ebe0f7..c3a195f 100644 --- a/cinderclient/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -1,5 +1,4 @@ import cStringIO -import os import re import sys diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index 95b50d0..fc61285 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -79,7 +79,7 @@ class FindResourceTestCase(test_utils.TestCase): class CaptureStdout(object): - """Context manager for capturing stdout from statments in its's block""" + """Context manager for capturing stdout from statments in its's block.""" def __enter__(self): self.real_stdout = sys.stdout self.stringio = StringIO.StringIO() diff --git a/cinderclient/tests/utils.py b/cinderclient/tests/utils.py index 3a12923..0ab8737 100644 --- a/cinderclient/tests/utils.py +++ b/cinderclient/tests/utils.py @@ -23,8 +23,9 @@ class TestCase(testtools.TestCase): class TestResponse(requests.Response): - """ Class used to wrap requests.Response and provide some - convenience to initialize with a dict """ + """Class used to wrap requests.Response and provide some + convenience to initialize with a dict. + """ def __init__(self, data): self._text = None diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 9e8e2f7..b3c7abf 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -15,8 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os - import fixtures from cinderclient import client diff --git a/cinderclient/tests/v1/test_types.py b/cinderclient/tests/v1/test_types.py index 41a89b7..6d7dd28 100644 --- a/cinderclient/tests/v1/test_types.py +++ b/cinderclient/tests/v1/test_types.py @@ -1,4 +1,3 @@ -from cinderclient import exceptions from cinderclient.v1 import volume_types from cinderclient.tests import utils from cinderclient.tests.v1 import fakes diff --git a/cinderclient/v1/__init__.py b/cinderclient/v1/__init__.py index cecfacd..fbb7b00 100644 --- a/cinderclient/v1/__init__.py +++ b/cinderclient/v1/__init__.py @@ -14,4 +14,4 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient.v1.client import Client +from cinderclient.v1.client import Client # noqa diff --git a/cinderclient/v1/limits.py b/cinderclient/v1/limits.py index 2008a69..007c533 100644 --- a/cinderclient/v1/limits.py +++ b/cinderclient/v1/limits.py @@ -4,7 +4,7 @@ from cinderclient import base class Limits(base.Resource): - """A collection of RateLimit and AbsoluteLimit objects""" + """A collection of RateLimit and AbsoluteLimit objects.""" def __repr__(self): return "" @@ -26,7 +26,7 @@ class Limits(base.Resource): class RateLimit(object): - """Data model that represents a flattened view of a single rate limit""" + """Data model that represents a flattened view of a single rate limit.""" def __init__(self, verb, uri, regex, value, remain, unit, next_available): @@ -52,7 +52,7 @@ class RateLimit(object): class AbsoluteLimit(object): - """Data model that represents a single absolute limit""" + """Data model that represents a single absolute limit.""" def __init__(self, name, value): self.name = name @@ -66,7 +66,7 @@ class AbsoluteLimit(object): class LimitsManager(base.Manager): - """Manager object used to interact with limits resource""" + """Manager object used to interact with limits resource.""" resource_class = Limits diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index caadd48..6696024 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -21,7 +21,8 @@ class QuotaClassSet(base.Resource): @property def id(self): """QuotaClassSet does not have a 'id' attribute but base.Resource - needs it to self-refresh and QuotaSet is indexed by class_name""" + needs it to self-refresh and QuotaSet is indexed by class_name. + """ return self.class_name def update(self, *args, **kwargs): diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index 846fb13..a463b81 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -20,8 +20,9 @@ class QuotaSet(base.Resource): @property def id(self): - """QuotaSet does not have a 'id' attribute but base.Resource needs it - to self-refresh and QuotaSet is indexed by tenant_id""" + """QuotaSet does not have a 'id' attribute but base. Resource needs it + to self-refresh and QuotaSet is indexed by tenant_id. + """ return self.tenant_id def update(self, *args, **kwargs): diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 214a475..51f4702 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -462,7 +462,7 @@ def do_type_create(cs, args): help="Unique ID of the volume type to delete") @utils.service_type('volume') def do_type_delete(cs, args): - """Delete a specific volume type""" + """Delete a specific volume type.""" cs.volume_types.delete(args.id) @@ -493,14 +493,14 @@ def do_type_key(cs, args): def do_endpoints(cs, args): - """Discover endpoints that get returned from the authenticate services""" + """Discover endpoints that get returned from the authenticate services.""" catalog = cs.client.service_catalog.catalog for e in catalog['access']['serviceCatalog']: utils.print_dict(e['endpoints'][0], e['name']) def do_credentials(cs, args): - """Show user credentials returned from auth""" + """Show user credentials returned from auth.""" catalog = cs.client.service_catalog.catalog utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['token'], "Token") diff --git a/cinderclient/v2/__init__.py b/cinderclient/v2/__init__.py index 5408cd3..d09fab5 100644 --- a/cinderclient/v2/__init__.py +++ b/cinderclient/v2/__init__.py @@ -14,4 +14,4 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient.v2.client import Client +from cinderclient.v2.client import Client # noqa diff --git a/cinderclient/v2/limits.py b/cinderclient/v2/limits.py index d076db8..72f9ea6 100644 --- a/cinderclient/v2/limits.py +++ b/cinderclient/v2/limits.py @@ -4,7 +4,7 @@ from cinderclient import base class Limits(base.Resource): - """A collection of RateLimit and AbsoluteLimit objects""" + """A collection of RateLimit and AbsoluteLimit objects.""" def __repr__(self): return "" @@ -26,7 +26,7 @@ class Limits(base.Resource): class RateLimit(object): - """Data model that represents a flattened view of a single rate limit""" + """Data model that represents a flattened view of a single rate limit.""" def __init__(self, verb, uri, regex, value, remain, unit, next_available): @@ -52,7 +52,7 @@ class RateLimit(object): class AbsoluteLimit(object): - """Data model that represents a single absolute limit""" + """Data model that represents a single absolute limit.""" def __init__(self, name, value): self.name = name @@ -66,7 +66,7 @@ class AbsoluteLimit(object): class LimitsManager(base.Manager): - """Manager object used to interact with limits resource""" + """Manager object used to interact with limits resource.""" resource_class = Limits diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index cc0b753..0a2a8df 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -20,7 +20,7 @@ class QuotaClassSet(base.Resource): @property def id(self): - """Needed by base.Resource to self-refresh and be indexed""" + """Needed by base.Resource to self-refresh and be indexed.""" return self.class_name def update(self, *args, **kwargs): diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 7c9db90..476ab41 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -20,7 +20,7 @@ class QuotaSet(base.Resource): @property def id(self): - """Needed by base.Resource to self-refresh and be indexed""" + """Needed by base.Resource to self-refresh and be indexed.""" return self.tenant_id def update(self, *args, **kwargs): diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index a502378..432c3b1 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -507,7 +507,7 @@ def do_type_create(cs, args): help="Unique ID of the volume type to delete") @utils.service_type('volume') def do_type_delete(cs, args): - """Delete a specific volume type""" + """Delete a specific volume type.""" cs.volume_types.delete(args.id) @@ -537,14 +537,14 @@ def do_type_key(cs, args): def do_endpoints(cs, args): - """Discover endpoints that get returned from the authenticate services""" + """Discover endpoints that get returned from the authenticate services.""" catalog = cs.client.service_catalog.catalog for e in catalog['access']['serviceCatalog']: utils.print_dict(e['endpoints'][0], e['name']) def do_credentials(cs, args): - """Show user credentials returned from auth""" + """Show user credentials returned from auth.""" catalog = cs.client.service_catalog.catalog utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['token'], "Token") diff --git a/tox.ini b/tox.ini index 161a11b..ca047b7 100644 --- a/tox.ini +++ b/tox.ini @@ -25,5 +25,5 @@ downloadcache = ~/cache/pip [flake8] show-source = True -ignore = F,H +ignore = F811,F821,H302,H306,H404 exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From adaaab3fc47e0d8449721e81678e303b997f57fa Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 9 Jun 2013 10:18:27 -0500 Subject: [PATCH 17/51] python3: Drop mox dependency Drop mox dependency, it is not being used at all. Change-Id: Ibcc971cee46dabad9c612d95593640ac139638e3 Signed-off-by: Chuck Short --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f8dbf56..0064e8a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,6 @@ coverage>=3.6 discover fixtures>=0.3.12 mock>=0.8.0 -mox>=0.5.3 python-subunit sphinx>=1.1.2 testtools>=0.9.29 From 934649cf2ba1daa7a290651e53d4c551df8dd1fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Jun 2013 11:27:25 -0700 Subject: [PATCH 18/51] Remove explicit depend on distribute. Change-Id: I1e9f561babf205de0c4afd2546c862b96d694f58 --- test-requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index f8dbf56..03cf5b2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,3 @@ -distribute>=0.6.24 - # Install bounded pep8/pyflakes first, then let flake8 install pep8==1.4.5 pyflakes==0.7.2 From d12d7a73ff6a1de482bcdb978e0fb7afd8cfe648 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 11 Jun 2013 13:22:56 -0500 Subject: [PATCH 19/51] python3: compatibility for iteritems and iterkeys Use six to allow python2/pyton3 for iteritems and iterkeys. six.iteriems() replaces dictionary.iteritems() (python2) and dictionary.iterms() (python3) six.iterkeys() replaces dictionary.iterkeys (python2) and dictionary.keys() (python3) Change-Id: I26c80b78a7dedf3aa32eedf01a83ff6d1e592ba7 Signed-off-by: Chuck Short --- cinderclient/base.py | 5 ++++- cinderclient/utils.py | 3 ++- cinderclient/v1/volume_snapshots.py | 3 ++- cinderclient/v1/volumes.py | 3 ++- cinderclient/v2/volume_snapshots.py | 3 ++- cinderclient/v2/volumes.py | 3 ++- requirements.txt | 1 + tools/colorizer.py | 3 ++- 8 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 6024f59..7577773 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -22,6 +22,9 @@ import abc import contextlib import hashlib import os + +import six + from cinderclient import exceptions from cinderclient import utils @@ -248,7 +251,7 @@ class Resource(object): return None def _add_details(self, info): - for (k, v) in info.iteritems(): + for (k, v) in six.iteritems(info): try: setattr(self, k, v) except AttributeError: diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 1a0034a..44522e0 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -18,6 +18,7 @@ import re import sys import uuid +import six import prettytable from cinderclient import exceptions @@ -165,7 +166,7 @@ def print_list(objs, fields, formatters={}): 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 d.iteritems()] + [pt.add_row(list(r)) for r in six.iteritems(d)] print strutils.safe_encode(pt.get_string(sortby=property)) diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index 50fa566..be1ae52 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -19,6 +19,7 @@ Volume snapshot interface (1.1 extension). import urllib from cinderclient import base +import six class Snapshot(base.Resource): @@ -95,7 +96,7 @@ class SnapshotManager(base.ManagerWithFind): qparams = {} - for opt, val in search_opts.iteritems(): + for opt, val in six.iteritems(search_opts): if val: qparams[opt] = val diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 7ab7e3c..9903f10 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -18,6 +18,7 @@ Volume interface (1.1 extension). """ import urllib +import six from cinderclient import base @@ -167,7 +168,7 @@ class VolumeManager(base.ManagerWithFind): qparams = {} - for opt, val in search_opts.iteritems(): + for opt, val in six.iteritems(search_opts): if val: qparams[opt] = val diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index d3ae632..240bdc6 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -15,6 +15,7 @@ """Volume snapshot interface (1.1 extension).""" +import six import urllib from cinderclient import base @@ -83,7 +84,7 @@ class SnapshotManager(base.ManagerWithFind): qparams = {} - for opt, val in search_opts.iteritems(): + for opt, val in six.iteritems(search_opts): if val: qparams[opt] = val diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index cf9f9ac..a9b69ab 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -15,6 +15,7 @@ """Volume interface (v2 extension).""" +import six import urllib from cinderclient import base @@ -161,7 +162,7 @@ class VolumeManager(base.ManagerWithFind): qparams = {} - for opt, val in search_opts.iteritems(): + for opt, val in six.iteritems(search_opts): if val: qparams[opt] = val diff --git a/requirements.txt b/requirements.txt index d677ca2..27c12b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ argparse prettytable>=0.6,<0.8 requests>=0.8 simplejson>=2.0.9 +six diff --git a/tools/colorizer.py b/tools/colorizer.py index a49abb1..9547802 100755 --- a/tools/colorizer.py +++ b/tools/colorizer.py @@ -47,6 +47,7 @@ import subunit import sys import unittest +import six import testtools @@ -277,7 +278,7 @@ class NovaTestResult(testtools.TestResult): self.stopTestRun() def stopTestRun(self): - for cls in list(self.results.iterkeys()): + for cls in list(six.iterkeys(self.results)): self.writeTestCase(cls) self.stream.writeln() self.writeSlowTests() From 4b1cdab2fe1c97eadc33f40856d78b5718fd3ed2 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 9 Jun 2013 21:24:10 -0500 Subject: [PATCH 20/51] python3: Basic python3 compatibility. Basic python3 compatibilty. Change-Id: I4388f5956cf397f8e33d20085aae6c6a728dbbda Signed-off-by: Chuck Short --- cinderclient/client.py | 8 +++++--- cinderclient/shell.py | 10 ++++++---- cinderclient/tests/fakes.py | 8 +++++--- cinderclient/utils.py | 6 ++++-- cinderclient/v1/shell.py | 8 +++++--- cinderclient/v2/shell.py | 8 +++++--- tools/install_venv.py | 22 ++++++++++++---------- 7 files changed, 42 insertions(+), 28 deletions(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 755ffcb..7b9a915 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -19,6 +19,8 @@ OpenStack Client interface. Handles the REST calls and responses. """ +from __future__ import print_function + import logging import os import urlparse @@ -229,13 +231,13 @@ class HTTPClient(object): self.management_url = management_url.rstrip('/') return None except exceptions.AmbiguousEndpoints: - print "Found more than one valid endpoint. Use a more " \ - "restrictive filter" + print("Found more than one valid endpoint. Use a more " + "restrictive filter") raise except KeyError: raise exceptions.AuthorizationFailure() except exceptions.EndpointNotFound: - print "Could not find any suitable endpoint. Correct region?" + print("Could not find any suitable endpoint. Correct region?") raise elif resp.status_code == 305: diff --git a/cinderclient/shell.py b/cinderclient/shell.py index bf95298..9d4af09 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -18,6 +18,8 @@ Command-line interface to the OpenStack Cinder API. """ +from __future__ import print_function + import argparse import glob import imp @@ -470,7 +472,7 @@ class OpenStackCinderShell(object): commands.remove('bash-completion') commands.remove('bash_completion') - print ' '.join(commands | options) + print(' '.join(commands | options)) @utils.arg('command', metavar='', nargs='?', help='Display help for ') @@ -500,14 +502,14 @@ def main(): try: OpenStackCinderShell().main(map(strutils.safe_decode, sys.argv[1:])) except KeyboardInterrupt: - print >> sys.stderr, "... terminating cinder client" + print("... terminating cinder client", file=sys.stderr) sys.exit(130) - except Exception, e: + except Exception as e: logger.debug(e, exc_info=1) message = e.message if not isinstance(message, basestring): message = str(message) - print >> sys.stderr, "ERROR: %s" % strutils.safe_encode(message) + print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr) sys.exit(1) diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index 04b40a4..fcf6d2b 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -6,6 +6,8 @@ wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ +from __future__ import print_function + def assert_has_keys(dict, required=[], optional=[]): keys = dict.keys() @@ -58,9 +60,9 @@ class FakeClient(object): try: assert entry[2] == body except AssertionError: - print entry[2] - print "!=" - print body + print(entry[2]) + print("!=") + print(body) raise self.client.callstack = [] diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 44522e0..c25b5d4 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import print_function + import os import re import sys @@ -160,14 +162,14 @@ def print_list(objs, fields, formatters={}): pt.add_row(row) if len(pt._rows) > 0: - print strutils.safe_encode(pt.get_string(sortby=fields[0])) + print(strutils.safe_encode(pt.get_string(sortby=fields[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)] - print strutils.safe_encode(pt.get_string(sortby=property)) + print(strutils.safe_encode(pt.get_string(sortby=property))) def find_resource(manager, name_or_id): diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 51f4702..5c56f4a 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import print_function + import argparse import os import sys @@ -39,17 +41,17 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, sys.stdout.write(msg) sys.stdout.flush() - print + print() while True: obj = poll_fn(obj_id) status = obj.status.lower() progress = getattr(obj, 'progress', None) or 0 if status in final_ok_states: print_progress(100) - print "\nFinished" + print("\nFinished") break elif status == "error": - print "\nError %(action)s instance" % locals() + print("\nError %(action)s instance" % locals()) break else: print_progress(progress) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 432c3b1..4016810 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import print_function + import argparse import os import sys @@ -35,17 +37,17 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, sys.stdout.write(msg) sys.stdout.flush() - print + print() while True: obj = poll_fn(obj_id) status = obj.status.lower() progress = getattr(obj, 'progress', None) or 0 if status in final_ok_states: print_progress(100) - print "\nFinished" + print("\nFinished") break elif status == "error": - print "\nError %(action)s instance" % locals() + print("\nError %(action)s instance" % locals()) break else: print_progress(progress) diff --git a/tools/install_venv.py b/tools/install_venv.py index f22c18d..55603d2 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -22,6 +22,8 @@ Installation script for Nova's development virtualenv """ +from __future__ import print_function + import optparse import os import subprocess @@ -37,7 +39,7 @@ PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) def die(message, *args): - print >> sys.stderr, message % args + print(message % args, file=sys.stderr) sys.exit(1) @@ -77,12 +79,12 @@ class Distro(object): return if self.check_cmd('easy_install'): - print 'Installing virtualenv via easy_install...', + print('Installing virtualenv via easy_install...', end=' ') if run_command(['easy_install', 'virtualenv']): - print 'Succeeded' + print('Succeeded') return else: - print 'Failed' + print('Failed') die('ERROR: virtualenv not found.\n\nDevelopment' ' requires virtualenv, please install it using your' @@ -162,17 +164,17 @@ def create_virtualenv(venv=VENV, no_site_packages=True): """Creates the virtual environment and installs PIP only into the virtual environment """ - print 'Creating venv...', + print('Creating venv...', end=' ') if no_site_packages: run_command(['virtualenv', '-q', '--no-site-packages', VENV]) else: run_command(['virtualenv', '-q', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', + print('done.') + print('Installing pip in virtualenv...', end=' ') if not run_command(['tools/with_venv.sh', 'easy_install', 'pip>1.0']).strip(): die("Failed to install pip.") - print 'done.' + print('done.') def pip_install(*args): @@ -182,7 +184,7 @@ def pip_install(*args): def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' + print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and distribute. pip_install('pip') @@ -220,7 +222,7 @@ def print_help(): Also, make test will automatically use the virtualenv. """ - print help + print(help) def parse_args(): From b4ea550ba7cd7d322739fcfae8b8af7ae191c49b Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 12 Jun 2013 08:28:17 -0500 Subject: [PATCH 21/51] python3: fix imports compatibility Python3 reorganized the standard library and moved several functions to different modules. Six provides a consistent interface to them through the fake six.moves module. However, the urlparse, urllib2, etc modules have been combined into one module which Six does not support so do it the old fashioned way. Change-Id: Ieb7cc7ee2a4a97807873cfe2fc3fa0a5cf3c3980 Signed-off-by: Chuck Short --- cinderclient/client.py | 7 ++++++- cinderclient/tests/test_shell.py | 4 ++-- cinderclient/tests/test_utils.py | 5 +++-- cinderclient/tests/v1/fakes.py | 5 ++++- cinderclient/tests/v2/fakes.py | 5 ++++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 755ffcb..5f3404b 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -21,7 +21,12 @@ OpenStack Client interface. Handles the REST calls and responses. import logging import os -import urlparse + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + try: from eventlet import sleep except ImportError: diff --git a/cinderclient/tests/test_shell.py b/cinderclient/tests/test_shell.py index c3a195f..d6ef425 100644 --- a/cinderclient/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -1,8 +1,8 @@ -import cStringIO import re import sys import fixtures +from six import moves from testtools import matchers from cinderclient import exceptions @@ -29,7 +29,7 @@ class ShellTest(utils.TestCase): def shell(self, argstr): orig = sys.stdout try: - sys.stdout = cStringIO.StringIO() + sys.stdout = moves.StringIO() _shell = cinderclient.shell.OpenStackCinderShell() _shell.main(argstr.split()) except SystemExit: diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index fc61285..8df482d 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -1,7 +1,8 @@ import collections -import StringIO import sys +from six import moves + from cinderclient import exceptions from cinderclient import utils from cinderclient import base @@ -82,7 +83,7 @@ class CaptureStdout(object): """Context manager for capturing stdout from statments in its's block.""" def __enter__(self): self.real_stdout = sys.stdout - self.stringio = StringIO.StringIO() + self.stringio = moves.StringIO() sys.stdout = self.stringio return self diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 411c5e1..a71f502 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -13,7 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import urlparse +try: + import urlparse +except ImportError: + import urllib.parse as urlparse from cinderclient import client as base_client from cinderclient.tests import fakes diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index f90ace3..28cb20a 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import urlparse +try: + import urlparse +except ImportError: + import urllib.parse as urlparse from cinderclient import client as base_client from cinderclient.tests import fakes From 046ea37f937f2254ffcd572ac3601cac411cf448 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 11 Jun 2013 20:42:13 -0500 Subject: [PATCH 22/51] python3: Update for metaclasses Use six.with_metaclass to create a new class with a base class base and metaclass metaclass. Change-Id: Id1e70f8cae0ac3dd075157f57d41a02b15e655f4 Signed-off-by: Chuck Short --- cinderclient/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 6684765..6077d79 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -166,11 +166,10 @@ class Manager(utils.HookableMixin): return body -class ManagerWithFind(Manager): +class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ - __metaclass__ = abc.ABCMeta @abc.abstractmethod def list(self): From dc1105ebca791c934abd822799b6e143396d013c Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 11 Jun 2013 21:19:40 -0500 Subject: [PATCH 23/51] python3: Fix unicode strings Python3 enforces the distinction between byte strings and text strings more rigorously than python2. So use six.text_type where approiate. Change-Id: I46b3f5fe1f990fc1b7a3ee32904d608b070fc4c3 Signed-off-by: Chuck Short --- cinderclient/utils.py | 4 ++-- doc/source/conf.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index c25b5d4..6e87129 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -278,7 +278,7 @@ def slugify(value): """ import unicodedata if not isinstance(value, unicode): - value = unicode(value) + value = six.text_type(value) value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) return _slugify_hyphenate_re.sub('-', value) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7abb3d0..a89528d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -42,8 +42,8 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'python-cinderclient' -copyright = u'Rackspace, based on work by Jacob Kaplan-Moss' +project = 'python-cinderclient' +copyright = 'Rackspace, based on work by Jacob Kaplan-Moss' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -179,8 +179,8 @@ htmlhelp_basename = 'python-cinderclientdoc' # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ - ('index', 'python-cinderclient.tex', u'python-cinderclient Documentation', - u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), + ('index', 'python-cinderclient.tex', 'python-cinderclient Documentation', + 'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of From cb1149599e5c2eb2eb999142299d2b33a55a985e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 15 Jun 2013 18:25:06 -0500 Subject: [PATCH 24/51] python3: Fix import compatibility. Python3 reorganized the standard library and moved several functions to different modules. Six provides a consistent interface to them through the fake six.moves module. However, the urlparse, urllib2, etc modules have been combined into one module which Six does not support so do it the old fashioned way. Change-Id: Idfb4f8eb538dcc98a1f7befc4487635d7f483973 Signed-off-by: Chuck Short --- cinderclient/v1/volume_snapshots.py | 8 ++++++-- cinderclient/v1/volumes.py | 7 +++++-- cinderclient/v2/volume_snapshots.py | 7 +++++-- cinderclient/v2/volumes.py | 7 +++++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index be1ae52..66237a5 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -17,7 +17,11 @@ Volume snapshot interface (1.1 extension). """ -import urllib +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + from cinderclient import base import six @@ -100,7 +104,7 @@ class SnapshotManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 9903f10..1302dd8 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -17,7 +17,10 @@ Volume interface (1.1 extension). """ -import urllib +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode import six from cinderclient import base @@ -172,7 +175,7 @@ class VolumeManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index 240bdc6..b16383c 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -16,7 +16,10 @@ """Volume snapshot interface (1.1 extension).""" import six -import urllib +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode from cinderclient import base @@ -88,7 +91,7 @@ class SnapshotManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index a9b69ab..c659d8a 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -16,7 +16,10 @@ """Volume interface (v2 extension).""" import six -import urllib +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode from cinderclient import base @@ -166,7 +169,7 @@ class VolumeManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlencode(qparams) if qparams else "" detail = "" if detailed: From 10484e5c66c66d4a3471306767352b5381de955d Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 15 Jun 2013 19:25:11 -0500 Subject: [PATCH 25/51] python3: Fix traceback while running tests. Fix: TypeError: Unicode-objects must be encoded before hashing while running tests. This is due to the fact in python3 hashlib.md5 works with bytes and we are passing unicode strings. Change-Id: I0adde942423af28572473030f6685e12cd8f7dae Signed-off-by: Chuck Short --- cinderclient/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 6077d79..3af3de8 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -102,7 +102,8 @@ class Manager(utils.HookableMixin): # pair username = utils.env('OS_USERNAME', 'CINDER_USERNAME') url = utils.env('OS_URL', 'CINDER_URL') - uniqifier = hashlib.md5(username + url).hexdigest() + uniqifier = hashlib.md5(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) From 1d5eea092193ea44c5e3dba23667328acb972d6c Mon Sep 17 00:00:00 2001 From: Cian O'Driscoll Date: Wed, 22 May 2013 15:09:56 +0000 Subject: [PATCH 26/51] Implements support migration for volume transfer Implements support for the volume transfer api added to cinder in https://review.openstack.org/#/c/29227 Change-Id: Idbcdfdd3ef76a8c516e08d1cf351d549254bc8c0 --- cinderclient/tests/v1/fakes.py | 77 +++++++++++++++++ .../tests/v1/test_volume_transfers.py | 51 ++++++++++++ cinderclient/tests/v2/fakes.py | 77 +++++++++++++++++ .../tests/v2/test_volume_transfers.py | 51 ++++++++++++ cinderclient/v1/client.py | 2 + cinderclient/v1/shell.py | 73 +++++++++++++++++ cinderclient/v1/volume_transfers.py | 82 +++++++++++++++++++ cinderclient/v2/client.py | 2 + cinderclient/v2/shell.py | 79 ++++++++++++++++++ cinderclient/v2/volume_transfers.py | 82 +++++++++++++++++++ 10 files changed, 576 insertions(+) create mode 100644 cinderclient/tests/v1/test_volume_transfers.py create mode 100644 cinderclient/tests/v2/test_volume_transfers.py create mode 100644 cinderclient/v1/volume_transfers.py create mode 100644 cinderclient/v2/volume_transfers.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index a71f502..54b5112 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -114,6 +114,44 @@ def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} +def _stub_transfer_full(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'transfer', + 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', + 'created_at': '2013-04-12T08:16:37.000000', + 'auth_key': '123456', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_transfer(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'transfer', + 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -405,3 +443,42 @@ class FakeHTTPClient(base_client.HTTPClient): def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw): return (200, {}, {'restore': _stub_restore()}) + + # + # VolumeTransfers + # + + def get_os_volume_transfer_5678(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (200, {}, + {'transfer': + _stub_transfer_full(transfer1, base_uri, tenant_id)}) + + def get_os_volume_transfer_detail(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41' + return (200, {}, + {'transfers': [ + _stub_transfer_full(transfer1, base_uri, tenant_id), + _stub_transfer_full(transfer2, base_uri, tenant_id)]}) + + def delete_os_volume_transfer_5678(self, **kw): + return (202, {}, None) + + def post_os_volume_transfer(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (202, {}, + {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) + + def post_os_volume_transfer_5678_accept(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (200, {}, + {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) diff --git a/cinderclient/tests/v1/test_volume_transfers.py b/cinderclient/tests/v1/test_volume_transfers.py new file mode 100644 index 0000000..40fb09b --- /dev/null +++ b/cinderclient/tests/v1/test_volume_transfers.py @@ -0,0 +1,51 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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 import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class VolumeTRansfersTest(utils.TestCase): + + def test_create(self): + cs.transfers.create('1234') + cs.assert_called('POST', '/os-volume-transfer') + + def test_get(self): + transfer_id = '5678' + cs.transfers.get(transfer_id) + cs.assert_called('GET', '/os-volume-transfer/%s' % transfer_id) + + def test_list(self): + cs.transfers.list() + cs.assert_called('GET', '/os-volume-transfer/detail') + + def test_delete(self): + b = cs.transfers.list()[0] + b.delete() + cs.assert_called('DELETE', '/os-volume-transfer/5678') + cs.transfers.delete('5678') + cs.assert_called('DELETE', '/os-volume-transfer/5678') + cs.transfers.delete(b) + cs.assert_called('DELETE', '/os-volume-transfer/5678') + + def test_accept(self): + transfer_id = '5678' + auth_key = '12345' + cs.transfers.accept(transfer_id, auth_key) + cs.assert_called('POST', '/os-volume-transfer/%s/accept' % transfer_id) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 28cb20a..580c904 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -121,6 +121,44 @@ def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} +def _stub_transfer_full(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'transfer', + 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', + 'created_at': '2013-04-12T08:16:37.000000', + 'auth_key': '123456', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_transfer(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'transfer', + 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -412,3 +450,42 @@ class FakeHTTPClient(base_client.HTTPClient): def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw): return (200, {}, {'restore': _stub_restore()}) + + # + # VolumeTransfers + # + + def get_os_volume_transfer_5678(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (200, {}, + {'transfer': + _stub_transfer_full(transfer1, base_uri, tenant_id)}) + + def get_os_volume_transfer_detail(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41' + return (200, {}, + {'transfers': [ + _stub_transfer_full(transfer1, base_uri, tenant_id), + _stub_transfer_full(transfer2, base_uri, tenant_id)]}) + + def delete_os_volume_transfer_5678(self, **kw): + return (202, {}, None) + + def post_os_volume_transfer(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (202, {}, + {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) + + def post_os_volume_transfer_5678_accept(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + transfer1 = '5678' + return (200, {}, + {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) diff --git a/cinderclient/tests/v2/test_volume_transfers.py b/cinderclient/tests/v2/test_volume_transfers.py new file mode 100644 index 0000000..40fb09b --- /dev/null +++ b/cinderclient/tests/v2/test_volume_transfers.py @@ -0,0 +1,51 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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 import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class VolumeTRansfersTest(utils.TestCase): + + def test_create(self): + cs.transfers.create('1234') + cs.assert_called('POST', '/os-volume-transfer') + + def test_get(self): + transfer_id = '5678' + cs.transfers.get(transfer_id) + cs.assert_called('GET', '/os-volume-transfer/%s' % transfer_id) + + def test_list(self): + cs.transfers.list() + cs.assert_called('GET', '/os-volume-transfer/detail') + + def test_delete(self): + b = cs.transfers.list()[0] + b.delete() + cs.assert_called('DELETE', '/os-volume-transfer/5678') + cs.transfers.delete('5678') + cs.assert_called('DELETE', '/os-volume-transfer/5678') + cs.transfers.delete(b) + cs.assert_called('DELETE', '/os-volume-transfer/5678') + + def test_accept(self): + transfer_id = '5678' + auth_key = '12345' + cs.transfers.accept(transfer_id, auth_key) + cs.assert_called('POST', '/os-volume-transfer/%s/accept' % transfer_id) diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index a5b9b02..19e7e62 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -22,6 +22,7 @@ from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volume_types from cinderclient.v1 import volume_backups from cinderclient.v1 import volume_backups_restore +from cinderclient.v1 import volume_transfers class Client(object): @@ -60,6 +61,7 @@ class Client(object): self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) + self.transfers = volume_transfers.VolumeTransferManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 5c56f4a..7e6255a 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -73,6 +73,11 @@ def _find_backup(cs, backup): return utils.find_resource(cs.backups, backup) +def _find_transfer(cs, transfer): + """Get a transfer by ID.""" + return utils.find_resource(cs.transfers, transfer) + + def _print_volume(volume): utils.print_dict(volume._info) @@ -719,3 +724,71 @@ def do_backup_restore(cs, args): """Restore a backup.""" cs.restores.restore(args.backup, args.volume_id) + + +@utils.arg('volume', metavar='', + help='ID of the volume to transfer.') +@utils.arg('--display-name', metavar='', + help='Optional transfer name. (Default=None)', + default=None) +@utils.service_type('volume') +def do_transfer_create(cs, args): + """Creates a volume transfer.""" + transfer = cs.transfers.create(args.volume, + args.display_name) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to delete.') +@utils.service_type('volume') +def do_transfer_delete(cs, args): + """Undo a transfer.""" + transfer = _find_transfer(cs, args.transfer) + transfer.delete() + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to accept.') +@utils.arg('auth_key', metavar='', + help='Auth key of the transfer to accept.') +@utils.service_type('volume') +def do_transfer_accept(cs, args): + """Accepts a volume transfer.""" + transfer = cs.transfers.accept(args.transfer, args.auth_key) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.service_type('volume') +def do_transfer_list(cs, args): + """List all the transfers.""" + transfers = cs.transfers.list() + columns = ['ID', 'Volume ID', 'Name'] + utils.print_list(transfers, columns) + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to accept.') +@utils.service_type('volume') +def do_transfer_show(cs, args): + """Show details about a transfer.""" + transfer = _find_transfer(cs, args.transfer) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) diff --git a/cinderclient/v1/volume_transfers.py b/cinderclient/v1/volume_transfers.py new file mode 100644 index 0000000..8aace02 --- /dev/null +++ b/cinderclient/v1/volume_transfers.py @@ -0,0 +1,82 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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. + +""" +Volume transfer interface (1.1 extension). +""" + +from cinderclient import base + + +class VolumeTransfer(base.Resource): + """Transfer a volume from one tenant to another""" + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume transfer.""" + return self.manager.delete(self) + + +class VolumeTransferManager(base.ManagerWithFind): + """Manage :class:`VolumeTransfer` resources.""" + resource_class = VolumeTransfer + + def create(self, volume_id, name=None): + """Create a volume transfer. + + :param volume_id: The ID of the volume to transfer. + :param name: The name of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'transfer': {'volume_id': volume_id, + 'name': name}} + return self._create('/os-volume-transfer', body, 'transfer') + + def accept(self, transfer_id, auth_key): + """Accept a volume transfer. + + :param transfer_id: The ID of the trasnfer to accept. + :param auth_key: The auth_key of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'accept': {'auth_key': auth_key}} + return self._create('/os-volume-transfer/%s/accept' % transfer_id, + body, 'transfer') + + def get(self, transfer_id): + """Show details of a volume transfer. + + :param transfer_id: The ID of the volume transfer to display. + :rtype: :class:`VolumeTransfer` + """ + return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") + + def list(self, detailed=True): + """Get a list of all volume transfer. + + :rtype: list of :class:`VolumeTransfer` + """ + if detailed is True: + return self._list("/os-volume-transfer/detail", "transfers") + else: + return self._list("/os-volume-transfer", "transfers") + + def delete(self, transfer_id): + """Delete a volume transfer. + + :param transfer_id: The :class:`VolumeTransfer` to delete. + """ + self._delete("/os-volume-transfer/%s" % base.getid(transfer_id)) diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index eb2760c..9079a52 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -22,6 +22,7 @@ from cinderclient.v2 import volume_snapshots from cinderclient.v2 import volume_types from cinderclient.v2 import volume_backups from cinderclient.v2 import volume_backups_restore +from cinderclient.v1 import volume_transfers class Client(object): @@ -58,6 +59,7 @@ class Client(object): self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) + self.transfers = volume_transfers.VolumeTransferManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 4016810..8d7e776 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -69,6 +69,11 @@ def _find_backup(cs, backup): return utils.find_resource(cs.backups, backup) +def _find_transfer(cs, transfer): + """Get a transfer by ID.""" + return utils.find_resource(cs.transfers, transfer) + + def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) @@ -785,3 +790,77 @@ def do_backup_restore(cs, args): """Restore a backup.""" cs.restores.restore(args.backup, args.volume_id) + + +@utils.arg('volume', metavar='', + help='ID of the volume to transfer.') +@utils.arg('--name', + metavar='', + default=None, + help='Optional transfer name. (Default=None)') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.service_type('volume') +def do_transfer_create(cs, args): + """Creates a volume transfer.""" + if args.display_name is not None: + args.name = args.display_name + + transfer = cs.transfers.create(args.volume, + args.name) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to delete.') +@utils.service_type('volume') +def do_transfer_delete(cs, args): + """Undo a transfer.""" + transfer = _find_transfer(cs, args.transfer) + transfer.delete() + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to accept.') +@utils.arg('auth_key', metavar='', + help='Auth key of the transfer to accept.') +@utils.service_type('volume') +def do_transfer_accept(cs, args): + """Accepts a volume transfer.""" + transfer = cs.transfers.accept(args.transfer, args.auth_key) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.service_type('volume') +def do_transfer_list(cs, args): + """List all the transfers.""" + transfers = cs.transfers.list() + columns = ['ID', 'Volume ID', 'Name'] + utils.print_list(transfers, columns) + + +@utils.arg('transfer', metavar='', + help='ID of the transfer to accept.') +@utils.service_type('volume') +def do_transfer_show(cs, args): + """Show details about a transfer.""" + transfer = _find_transfer(cs, args.transfer) + info = dict() + info.update(transfer._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) diff --git a/cinderclient/v2/volume_transfers.py b/cinderclient/v2/volume_transfers.py new file mode 100644 index 0000000..8aace02 --- /dev/null +++ b/cinderclient/v2/volume_transfers.py @@ -0,0 +1,82 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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. + +""" +Volume transfer interface (1.1 extension). +""" + +from cinderclient import base + + +class VolumeTransfer(base.Resource): + """Transfer a volume from one tenant to another""" + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume transfer.""" + return self.manager.delete(self) + + +class VolumeTransferManager(base.ManagerWithFind): + """Manage :class:`VolumeTransfer` resources.""" + resource_class = VolumeTransfer + + def create(self, volume_id, name=None): + """Create a volume transfer. + + :param volume_id: The ID of the volume to transfer. + :param name: The name of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'transfer': {'volume_id': volume_id, + 'name': name}} + return self._create('/os-volume-transfer', body, 'transfer') + + def accept(self, transfer_id, auth_key): + """Accept a volume transfer. + + :param transfer_id: The ID of the trasnfer to accept. + :param auth_key: The auth_key of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'accept': {'auth_key': auth_key}} + return self._create('/os-volume-transfer/%s/accept' % transfer_id, + body, 'transfer') + + def get(self, transfer_id): + """Show details of a volume transfer. + + :param transfer_id: The ID of the volume transfer to display. + :rtype: :class:`VolumeTransfer` + """ + return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") + + def list(self, detailed=True): + """Get a list of all volume transfer. + + :rtype: list of :class:`VolumeTransfer` + """ + if detailed is True: + return self._list("/os-volume-transfer/detail", "transfers") + else: + return self._list("/os-volume-transfer", "transfers") + + def delete(self, transfer_id): + """Delete a volume transfer. + + :param transfer_id: The :class:`VolumeTransfer` to delete. + """ + self._delete("/os-volume-transfer/%s" % base.getid(transfer_id)) From 3044671b3647cd9a6b14b6262baa44567c6e995b Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 19 Jun 2013 20:46:46 -0500 Subject: [PATCH 27/51] python3: Fix traceback while running tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The testsuite is full of the following: TypeError: 'dict_keys' object does not support indexing This is due to the fact in python3 dict methods dict.keys(), dict.items() and dict.values() return “views” instead of lists. Change-Id: Ifa5383e6485fdbabf363fd1442877b2452346c1c Signed-off-by: Chuck Short --- cinderclient/base.py | 6 +++--- cinderclient/client.py | 2 +- cinderclient/exceptions.py | 2 +- cinderclient/extension.py | 2 +- cinderclient/shell.py | 4 ++-- cinderclient/tests/fakes.py | 2 +- cinderclient/tests/v1/fakes.py | 16 ++++++++-------- cinderclient/tests/v2/fakes.py | 16 ++++++++-------- cinderclient/utils.py | 2 +- cinderclient/v1/limits.py | 2 +- cinderclient/v1/quota_classes.py | 2 +- cinderclient/v1/quotas.py | 2 +- cinderclient/v1/shell.py | 6 +++--- cinderclient/v2/limits.py | 2 +- cinderclient/v2/quota_classes.py | 2 +- cinderclient/v2/quotas.py | 2 +- cinderclient/v2/shell.py | 6 +++--- tools/colorizer.py | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 3af3de8..4e29078 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -201,7 +201,7 @@ class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)): the Python side. """ found = [] - searches = kwargs.items() + searches = list(kwargs.items()) for obj in self.list(): try: @@ -270,8 +270,8 @@ class Resource(object): return self.__dict__[k] def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') + reprkeys = sorted(k for k in list(self.__dict__.keys()) if k[0] != '_' + and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) diff --git a/cinderclient/client.py b/cinderclient/client.py index ec51b92..d940da4 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -387,7 +387,7 @@ def get_client_class(version): client_path = version_map[str(version)] except (KeyError, ValueError): msg = "Invalid client version '%s'. must be one of: %s" % ( - (version, ', '.join(version_map.keys()))) + (version, ', '.join(list(version_map.keys())))) raise exceptions.UnsupportedVersion(msg) return utils.import_class(client_path) diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index d381246..d56f34a 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -143,7 +143,7 @@ def from_response(response, body): message = "n/a" details = "n/a" if hasattr(body, 'keys'): - error = body[body.keys()[0]] + error = body[list(body.keys())[0]] message = error.get('message', None) details = error.get('details', None) return cls(code=response.status_code, message=message, details=details, diff --git a/cinderclient/extension.py b/cinderclient/extension.py index ced67f0..07d8450 100644 --- a/cinderclient/extension.py +++ b/cinderclient/extension.py @@ -29,7 +29,7 @@ class Extension(utils.HookableMixin): def _parse_extension_module(self): self.manager_class = None - for attr_name, attr_value in self.module.__dict__.items(): + for attr_name, attr_value in list(self.module.__dict__.items()): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) elif utils.safe_issubclass(attr_value, base.Manager): diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 9d4af09..9750d2f 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -465,9 +465,9 @@ class OpenStackCinderShell(object): """ commands = set() options = set() - for sc_str, sc in self.subcommands.items(): + for sc_str, sc in list(self.subcommands.items()): commands.add(sc_str) - for option in sc._optionals._option_string_actions.keys(): + for option in list(sc._optionals._option_string_actions.keys()): options.add(option) commands.remove('bash-completion') diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index fcf6d2b..a6872bc 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -10,7 +10,7 @@ from __future__ import print_function def assert_has_keys(dict, required=[], optional=[]): - keys = dict.keys() + keys = list(dict.keys()) for k in required: try: assert k in keys diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index a71f502..8534c56 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -215,10 +215,10 @@ class FakeHTTPClient(base_client.HTTPClient): def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 - assert len(body.keys()) == 1 - action = body.keys()[0] + assert len(list(body.keys())) == 1 + action = list(body.keys())[0] if action == 'os-attach': - assert body[action].keys() == ['instance_uuid', 'mountpoint'] + assert list(body[action].keys()) == ['instance_uuid', 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': @@ -226,10 +226,10 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-unreserve': assert body[action] is None elif action == 'os-initialize_connection': - assert body[action].keys() == ['connector'] + assert list(body[action].keys()) == ['connector'] return (202, {}, {'connection_info': 'foos'}) elif action == 'os-terminate_connection': - assert body[action].keys() == ['connector'] + assert list(body[action].keys()) == ['connector'] elif action == 'os-begin_detaching': assert body[action] is None elif action == 'os-roll_detaching': @@ -265,7 +265,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_sets_test(self, body, **kw): - assert body.keys() == ['quota_set'] + assert list(body.keys()) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { @@ -288,7 +288,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_class_sets_test(self, body, **kw): - assert body.keys() == ['quota_class_set'] + assert list(body.keys()) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -321,7 +321,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'extra_specs': {}}}) def post_types_1_extra_specs(self, body, **kw): - assert body.keys() == ['extra_specs'] + assert list(body.keys()) == ['extra_specs'] return (200, {}, {'extra_specs': {'k': 'v'}}) def delete_types_1_extra_specs_k(self, **kw): diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 28cb20a..4880dea 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -222,10 +222,10 @@ class FakeHTTPClient(base_client.HTTPClient): def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 - assert len(body.keys()) == 1 - action = body.keys()[0] + assert len(list(body.keys())) == 1 + action = list(body.keys())[0] if action == 'os-attach': - assert body[action].keys() == ['instance_uuid', 'mountpoint'] + assert list(body[action].keys()) == ['instance_uuid', 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': @@ -233,10 +233,10 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-unreserve': assert body[action] is None elif action == 'os-initialize_connection': - assert body[action].keys() == ['connector'] + assert list(body[action].keys()) == ['connector'] return (202, {}, {'connection_info': 'foos'}) elif action == 'os-terminate_connection': - assert body[action].keys() == ['connector'] + assert list(body[action].keys()) == ['connector'] elif action == 'os-begin_detaching': assert body[action] is None elif action == 'os-roll_detaching': @@ -272,7 +272,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_sets_test(self, body, **kw): - assert body.keys() == ['quota_set'] + assert list(body.keys()) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { @@ -295,7 +295,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_class_sets_test(self, body, **kw): - assert body.keys() == ['quota_class_set'] + assert list(body.keys()) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -328,7 +328,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'extra_specs': {}}}) def post_types_1_extra_specs(self, body, **kw): - assert body.keys() == ['extra_specs'] + assert list(body.keys()) == ['extra_specs'] return (200, {}, {'extra_specs': {'k': 'v'}}) def delete_types_1_extra_specs_k(self, **kw): diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 6e87129..fe947a0 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -217,7 +217,7 @@ def find_resource(manager, name_or_id): def _format_servers_list_networks(server): output = [] - for (network, addresses) in server.networks.items(): + for (network, addresses) in list(server.networks.items()): if len(addresses) == 0: continue addresses_csv = ', '.join(addresses) diff --git a/cinderclient/v1/limits.py b/cinderclient/v1/limits.py index 007c533..f76cd68 100644 --- a/cinderclient/v1/limits.py +++ b/cinderclient/v1/limits.py @@ -11,7 +11,7 @@ class Limits(base.Resource): @property def absolute(self): - for (name, value) in self._info['absolute'].items(): + for (name, value) in list(self._info['absolute'].items()): yield AbsoluteLimit(name, value) @property diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index 6696024..da96e5c 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -46,7 +46,7 @@ class QuotaClassSetManager(base.Manager): 'volumes': volumes, 'gigabytes': gigabytes}} - for key in body['quota_class_set'].keys(): + for key in list(body['quota_class_set'].keys()): if body['quota_class_set'][key] is None: body['quota_class_set'].pop(key) diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index a463b81..ce7a912 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -45,7 +45,7 @@ class QuotaSetManager(base.Manager): 'snapshots': snapshots, 'gigabytes': gigabytes}} - for key in body['quota_set'].keys(): + for key in list(body['quota_set'].keys()): if body['quota_set'][key] is None: body['quota_set'].pop(key) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 5c56f4a..a9e366e 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -83,7 +83,7 @@ def _print_volume_snapshot(snapshot): def _translate_keys(collection, convert): for item in collection: - keys = item.__dict__.keys() + keys = list(item.__dict__.keys()) for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item._info[from_key]) @@ -306,7 +306,7 @@ def do_metadata(cs, args): if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': - cs.volumes.delete_metadata(volume, metadata.keys()) + cs.volumes.delete_metadata(volume, list(metadata.keys())) @utils.arg( @@ -491,7 +491,7 @@ def do_type_key(cs, args): if args.action == 'set': vtype.set_keys(keypair) elif args.action == 'unset': - vtype.unset_keys(keypair.keys()) + vtype.unset_keys(list(keypair.keys())) def do_endpoints(cs, args): diff --git a/cinderclient/v2/limits.py b/cinderclient/v2/limits.py index 72f9ea6..87349b6 100644 --- a/cinderclient/v2/limits.py +++ b/cinderclient/v2/limits.py @@ -11,7 +11,7 @@ class Limits(base.Resource): @property def absolute(self): - for (name, value) in self._info['absolute'].items(): + for (name, value) in list(self._info['absolute'].items()): yield AbsoluteLimit(name, value) @property diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index 0a2a8df..eeda85c 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -44,7 +44,7 @@ class QuotaClassSetManager(base.Manager): 'volumes': volumes, 'gigabytes': gigabytes}} - for key in body['quota_class_set'].keys(): + for key in list(body['quota_class_set'].keys()): if body['quota_class_set'][key] is None: body['quota_class_set'].pop(key) diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 476ab41..30c4186 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -43,7 +43,7 @@ class QuotaSetManager(base.Manager): 'snapshots': snapshots, 'gigabytes': gigabytes}} - for key in body['quota_set'].keys(): + for key in list(body['quota_set'].keys()): if body['quota_set'][key] is None: body['quota_set'].pop(key) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 0555a78..e96609d 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -75,7 +75,7 @@ def _print_volume_snapshot(snapshot): def _translate_keys(collection, convert): for item in collection: - keys = item.__dict__.keys() + keys = list(item.__dict__.keys()) for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item._info[from_key]) @@ -351,7 +351,7 @@ def do_metadata(cs, args): if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': - cs.volumes.delete_metadata(volume, metadata.keys()) + cs.volumes.delete_metadata(volume, list(metadata.keys())) @utils.arg('--all-tenants', @@ -557,7 +557,7 @@ def do_type_key(cs, args): if args.action == 'set': vtype.set_keys(keypair) elif args.action == 'unset': - vtype.unset_keys(keypair.keys()) + vtype.unset_keys(list(keypair.keys())) def do_endpoints(cs, args): diff --git a/tools/colorizer.py b/tools/colorizer.py index 9547802..1b6e576 100755 --- a/tools/colorizer.py +++ b/tools/colorizer.py @@ -267,7 +267,7 @@ class NovaTestResult(testtools.TestResult): if not self.last_written or (self._now() - time).total_seconds() > 2.0: diff = 3.0 while diff > 2.0: - classes = self.results.keys() + classes =list(self.results.keys()) oldest = min(classes, key=lambda x: self.last_time[x]) diff = (self._now() - self.last_time[oldest]).total_seconds() self.writeTestCase(oldest) From 1197fb3701641663c86c678c27708b6bf71c7062 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 21 Jun 2013 07:15:11 -0500 Subject: [PATCH 28/51] python3: Strutils is not needed strutils is used to safely encode and decode unicode strings for python2.7. Since unicode strings are the default in python3, ignore the use of strutils when running with python3. Change-Id: I9a8e296b4f2153b1ef4302a7dcd797fbb4561c35 Signed-off-by: Chuck Short --- cinderclient/shell.py | 10 ++++++++-- cinderclient/utils.py | 23 +++++++++++++++++------ cinderclient/v2/shell.py | 4 +++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 9750d2f..d7377b1 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -29,6 +29,8 @@ import pkgutil import sys import logging +import six + from cinderclient import client from cinderclient import exceptions as exc import cinderclient.extension @@ -500,14 +502,18 @@ class OpenStackHelpFormatter(argparse.HelpFormatter): def main(): try: - OpenStackCinderShell().main(map(strutils.safe_decode, sys.argv[1:])) + if sys.version_info >= (3, 0): + OpenStackCinderShell().main(sys.argv[1:]) + else: + OpenStackCinderShell().main(map(strutils.safe_decode, + sys.argv[1:])) except KeyboardInterrupt: print("... terminating cinder client", file=sys.stderr) sys.exit(130) except Exception as e: logger.debug(e, exc_info=1) message = e.message - if not isinstance(message, basestring): + if not isinstance(message, six.string_types): message = str(message) print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr) sys.exit(1) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index fe947a0..922f053 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -142,7 +142,14 @@ def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) -def print_list(objs, fields, formatters={}): +def _print(pt, order): + if sys.version_info >= (3, 0): + print(pt.get_string(sortby=order)) + else: + print(strutils.safe_encode(pt.get_string(sortby=order))) + + +def print_list(objs, fields, formatters={}, order_by=None): mixed_case_fields = ['serverId'] pt = prettytable.PrettyTable([f for f in fields], caching=False) pt.aligns = ['l' for f in fields] @@ -161,15 +168,16 @@ def print_list(objs, fields, formatters={}): row.append(data) pt.add_row(row) - if len(pt._rows) > 0: - print(strutils.safe_encode(pt.get_string(sortby=fields[0]))) + if order_by is None: + order_by = fields[0] + _print(pt, order_by) 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)] - print(strutils.safe_encode(pt.get_string(sortby=property))) + _print(pt, property) def find_resource(manager, name_or_id): @@ -181,9 +189,12 @@ def find_resource(manager, name_or_id): except exceptions.NotFound: pass + if sys.version_info <= (3, 0): + name_or_id = strutils.safe_decode(name_or_id) + # now try to get entity as uuid try: - uuid.UUID(strutils.safe_decode(name_or_id)) + uuid.UUID(name_or_id) return manager.get(name_or_id) except (ValueError, exceptions.NotFound): pass @@ -277,7 +288,7 @@ def slugify(value): From Django's "django/template/defaultfilters.py". """ import unicodedata - if not isinstance(value, unicode): + if not isinstance(value, six.text_type): value = six.text_type(value) value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index e96609d..996447d 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -20,6 +20,8 @@ import os import sys import time +import six + from cinderclient import exceptions from cinderclient import utils @@ -253,7 +255,7 @@ def do_create(cs, args): # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: - if isinstance(hints[key], basestring): + if isinstance(hints[key], six.string_types): hints[key] = [hints[key]] hints[key] += [value] else: From 6adda93c9d52a1bb841261725f666a439b54539c Mon Sep 17 00:00:00 2001 From: Anastasia Latynskaya Date: Mon, 10 Jun 2013 13:08:29 +0400 Subject: [PATCH 29/51] Connectivity between the endpoint version and OS_VOLUME_API_VERSION. Adds functionality which allows user to work with that cinder API version which is the same as the endpoint version. Fixes: bug #1169455 Change-Id: I9bb46e602d15856d2da502a6ac2b6c25e76f4fa3 --- cinderclient/client.py | 11 +++++++++++ cinderclient/exceptions.py | 4 ++++ cinderclient/shell.py | 9 +++++++++ cinderclient/tests/v1/fakes.py | 9 +++++++++ cinderclient/tests/v1/test_shell.py | 2 +- cinderclient/tests/v2/fakes.py | 9 +++++++++ cinderclient/v1/client.py | 3 +++ cinderclient/v2/client.py | 3 +++ 8 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 0cdb861..3b3b828 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -369,6 +369,17 @@ class HTTPClient(object): return self._extract_service_catalog(url, resp, body) + def get_volume_api_version_from_endpoint(self): + magic_tuple = urlparse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + v = path.split("/")[1] + valid_versions = ['v1', 'v2'] + if v not in valid_versions: + msg = "Invalid client version '%s'. must be one of: %s" % ( + (v, ', '.join(valid_versions))) + raise exceptions.UnsupportedVersion(msg) + return v[1:] + def get_client_class(version): version_map = { diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index d7be180..534888e 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -10,6 +10,10 @@ class UnsupportedVersion(Exception): pass +class InvalidAPIVersion(Exception): + pass + + class CommandError(Exception): pass diff --git a/cinderclient/shell.py b/cinderclient/shell.py index bf95298..84f926b 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -448,6 +448,15 @@ class OpenStackCinderShell(object): except exc.AuthorizationFailure: raise exc.CommandError("Unable to authorize user") + endpoint_api_version = self.cs.get_volume_api_version_from_endpoint() + if endpoint_api_version != options.os_volume_api_version: + msg = (("Volume API version is set to %s " + "but you are accessing a %s endpoint. " + "Change its value via either --os-volume-api-version " + "or env[OS_VOLUME_API_VERSION]") + % (options.os_volume_api_version, endpoint_api_version)) + raise exc.InvalidAPIVersion(msg) + args.func(self.cs, args) def _run_extension_hooks(self, hook_type, *args, **kwargs): diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 411c5e1..2e15f3e 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -119,6 +119,9 @@ class FakeClient(fakes.FakeClient, client.Client): extensions=kwargs.get('extensions')) self.client = FakeHTTPClient(**kwargs) + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() + class FakeHTTPClient(base_client.HTTPClient): @@ -127,6 +130,7 @@ class FakeHTTPClient(base_client.HTTPClient): self.password = 'password' self.auth_url = 'auth_url' self.callstack = [] + self.management_url = 'http://10.0.2.15:8776/v1/fake' def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly @@ -164,6 +168,11 @@ class FakeHTTPClient(base_client.HTTPClient): else: return utils.TestResponse({"status": status}), body + def get_volume_api_version_from_endpoint(self): + magic_tuple = urlparse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + return path.lstrip('/').split('/')[0][1:] + # # Snapshots # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 9e8e2f7..d107ae5 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -32,7 +32,7 @@ class ShellTest(utils.TestCase): 'CINDER_USERNAME': 'username', 'CINDER_PASSWORD': 'password', 'CINDER_PROJECT_ID': 'project_id', - 'OS_VOLUME_API_VERSION': '1.1', + 'OS_VOLUME_API_VERSION': '1', 'CINDER_URL': 'http://no.where', } diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index f90ace3..eab1a5a 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -126,6 +126,9 @@ class FakeClient(fakes.FakeClient, client.Client): extensions=kwargs.get('extensions')) self.client = FakeHTTPClient(**kwargs) + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() + class FakeHTTPClient(base_client.HTTPClient): @@ -134,6 +137,7 @@ class FakeHTTPClient(base_client.HTTPClient): self.password = 'password' self.auth_url = 'auth_url' self.callstack = [] + self.management_url = 'http://10.0.2.15:8776/v2/fake' def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly @@ -171,6 +175,11 @@ class FakeHTTPClient(base_client.HTTPClient): else: return utils.TestResponse({"status": status}), body + def get_volume_api_version_from_endpoint(self): + magic_tuple = urlparse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + return path.lstrip('/').split('/')[0][1:] + # # Snapshots # diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index a5b9b02..2906e2f 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -98,3 +98,6 @@ class Client(object): credentials are wrong. """ self.client.authenticate() + + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index eb2760c..5a8b48e 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -95,3 +95,6 @@ class Client(object): credentials are wrong. """ self.client.authenticate() + + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() From 7571232c024cb66d5c68546e313b1a9a7ba2b2fc Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 13 Jun 2013 15:55:16 -0700 Subject: [PATCH 30/51] Implement reset-state (os-reset_status) action This change implements a reset-state method in the cinderclient which exposes the extension: os-reset_status from the Cinder API. Fix bug 1190731 Change-Id: I8f631f0f19ca16e0fdd1e3adcf923cc684292887 --- cinderclient/tests/v1/fakes.py | 4 +++- cinderclient/tests/v1/test_shell.py | 10 ++++++++++ cinderclient/tests/v2/fakes.py | 4 +++- cinderclient/tests/v2/test_shell.py | 10 ++++++++++ cinderclient/v1/shell.py | 12 ++++++++++++ cinderclient/v1/volumes.py | 8 ++++++++ cinderclient/v2/shell.py | 12 ++++++++++++ cinderclient/v2/volumes.py | 8 ++++++++ 8 files changed, 66 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index a71f502..9910b1d 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -234,8 +234,10 @@ class FakeHTTPClient(base_client.HTTPClient): assert body[action] is None elif action == 'os-roll_detaching': assert body[action] is None + elif action == 'os-reset_status': + assert 'status' in body[action] else: - raise AssertionError("Unexpected server action: %s" % action) + raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) def post_volumes(self, **kw): diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index b3c7abf..f94b8ad 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -179,3 +179,13 @@ class ShellTest(utils.TestCase): self.run_command('metadata 1234 unset key1 key2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + + def test_reset_state(self): + self.run_command('reset-state 1234') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_reset_state_with_flag(self): + self.run_command('reset-state --state error 1234') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 28cb20a..7482283 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -241,8 +241,10 @@ class FakeHTTPClient(base_client.HTTPClient): assert body[action] is None elif action == 'os-roll_detaching': assert body[action] is None + elif action == 'os-reset_status': + assert 'status' in body[action] else: - raise AssertionError("Unexpected server action: %s" % action) + raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) def post_volumes(self, **kw): diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 8f6e074..ce7842c 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -157,3 +157,13 @@ class ShellTest(utils.TestCase): self.run_command('metadata 1234 unset key1 key2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + + def test_reset_state(self): + self.run_command('reset-state 1234') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_reset_state_with_flag(self): + self.run_command('reset-state --state error 1234') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 5c56f4a..15a5fc1 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -268,6 +268,18 @@ def do_force_delete(cs, args): volume.force_delete() +@utils.arg('volume', metavar='', help='ID of the volume to modify.') +@utils.arg('--state', metavar='', default='available', + help=('Indicate which state to assign the volume. Options include ' + 'available, error, creating, deleting, error_deleting. If no ' + 'state is provided, available will be used.')) +@utils.service_type('volume') +def do_reset_state(cs, args): + """Explicitly update the state of a volume.""" + volume = _find_volume(cs, args.volume) + volume.reset_state(args.state) + + @utils.arg('volume', metavar='', help='ID of the volume to rename.') @utils.arg('display_name', nargs='?', metavar='', help='New display-name for the volume.') diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 9903f10..9ae5f47 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -98,6 +98,10 @@ 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) + class VolumeManager(base.ManagerWithFind): """ @@ -327,3 +331,7 @@ class VolumeManager(base.ManagerWithFind): def force_delete(self, volume): 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}) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 4016810..986b925 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -278,6 +278,18 @@ def do_force_delete(cs, args): volume.force_delete() +@utils.arg('volume', metavar='', help='ID of the volume to modify.') +@utils.arg('--state', metavar='', default='available', + help=('Indicate which state to assign the volume. Options include ' + 'available, error, creating, deleting, error_deleting. If no ' + 'state is provided, available will be used.')) +@utils.service_type('volume') +def do_reset_state(cs, args): + """Explicitly update the state of a volume.""" + volume = _find_volume(cs, args.volume) + volume.reset_state(args.state) + + @utils.arg('volume', metavar='', help='ID of the volume to rename.') diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index a9b69ab..1621830 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -97,6 +97,10 @@ 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) + class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" @@ -307,3 +311,7 @@ class VolumeManager(base.ManagerWithFind): def force_delete(self, volume): 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}) From 8e7cc89d058d773ada13d615c1d978ae9d73917c Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Fri, 14 Jun 2013 08:42:49 +0300 Subject: [PATCH 31/51] Fix volume info display error on create with v2. Error due to popping 'links' when it does not exist. Change-Id: I3f25b97f16699373ef12d9ac47905900b4b631f8 Fixes: bug 1190853 --- cinderclient/v2/shell.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index b86f7f9..4489823 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -170,9 +170,7 @@ def do_show(cs, args): volume = _find_volume(cs, args.volume) info.update(volume._info) - if 'links' in info: - info.pop('links') - + info.pop('links', None) utils.print_dict(info) @@ -282,8 +280,7 @@ def do_create(cs, args): volume = cs.volumes.get(volume.id) info.update(volume._info) - info.pop('links') - + info.pop('links', None) utils.print_dict(info) @@ -792,9 +789,7 @@ def do_backup_show(cs, args): info = dict() info.update(backup._info) - if 'links' in info: - info.pop('links') - + info.pop('links', None) utils.print_dict(info) @@ -847,9 +842,7 @@ def do_transfer_create(cs, args): info = dict() info.update(transfer._info) - if 'links' in info: - info.pop('links') - + info.pop('links', None) utils.print_dict(info) @@ -873,9 +866,7 @@ def do_transfer_accept(cs, args): info = dict() info.update(transfer._info) - if 'links' in info: - info.pop('links') - + info.pop('links', None) utils.print_dict(info) @@ -896,7 +887,5 @@ def do_transfer_show(cs, args): info = dict() info.update(transfer._info) - if 'links' in info: - info.pop('links') - + info.pop('links', None) utils.print_dict(info) From a7cce08eab5e2e42275b84bd56127bd09b00f5bf Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Sun, 19 May 2013 18:12:27 +0300 Subject: [PATCH 32/51] Use exceptions from oslo These exceptions can be used in novaclient, keystoneclient, glanceclient, and other client projects. Partially implements: blueprint common-client-library Change-Id: I43918316622b1c1d722872fe30199db6a3a7bb76 --- cinderclient/base.py | 2 +- cinderclient/client.py | 35 +- cinderclient/exceptions.py | 156 +----- .../openstack/common/apiclient/__init__.py | 16 + .../openstack/common/apiclient/exceptions.py | 446 ++++++++++++++++++ cinderclient/shell.py | 2 +- cinderclient/tests/test_base.py | 2 +- cinderclient/tests/test_http.py | 2 +- cinderclient/tests/test_service_catalog.py | 2 +- cinderclient/tests/test_shell.py | 2 +- cinderclient/tests/test_utils.py | 2 +- cinderclient/tests/utils.py | 3 +- cinderclient/tests/v1/test_auth.py | 2 +- cinderclient/tests/v2/test_auth.py | 2 +- cinderclient/utils.py | 2 +- cinderclient/v1/shell.py | 2 +- cinderclient/v2/shell.py | 2 +- openstack-common.conf | 2 +- 18 files changed, 499 insertions(+), 183 deletions(-) create mode 100644 cinderclient/openstack/common/apiclient/__init__.py create mode 100644 cinderclient/openstack/common/apiclient/exceptions.py diff --git a/cinderclient/base.py b/cinderclient/base.py index 4e29078..ccbdd98 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -25,7 +25,7 @@ import os import six -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import utils diff --git a/cinderclient/client.py b/cinderclient/client.py index 857f80a..9a40dfc 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -46,7 +46,7 @@ if not hasattr(urlparse, 'parse_qsl'): import requests -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import service_catalog from cinderclient import utils @@ -151,7 +151,7 @@ class HTTPClient(object): body = None if resp.status_code >= 400: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, method, url) return resp, body @@ -185,7 +185,7 @@ class HTTPClient(object): except exceptions.ClientException as e: if attempts > self.retries: raise - if 500 <= e.code <= 599: + if 500 <= e.http_status <= 599: pass else: raise @@ -211,15 +211,16 @@ class HTTPClient(object): def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) - def _extract_service_catalog(self, url, resp, body, extract_token=True): + def _extract_service_catalog(self, auth_url, url, method, + extract_token=True, **kwargs): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints. """ - + resp, body = self.request(url, method, **kwargs) if resp.status_code == 200: # content must always present try: - self.auth_url = url + self.auth_url = auth_url self.service_catalog = \ service_catalog.ServiceCatalog(body) @@ -248,7 +249,7 @@ class HTTPClient(object): elif resp.status_code == 305: return resp['location'] else: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, method, url) def _fetch_endpoints_from_auth(self, url): """We have a token, but don't know the final endpoint for @@ -263,13 +264,14 @@ class HTTPClient(object): """ # GET ...:5001/v2.0/tokens/#####/endpoints + auth_url = url url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) self._logger.debug("Using Endpoint URL: %s" % url) - resp, body = self.request(url, "GET", - headers={'X-Auth-Token': self.auth_token}) - return self._extract_service_catalog(url, resp, body, - extract_token=False) + return self._extract_service_catalog( + auth_url, + url, "GET", headers={'X-Auth-Token': self.auth_token}, + extract_token=False) def authenticate(self): magic_tuple = urlparse.urlsplit(self.auth_url) @@ -320,7 +322,9 @@ class HTTPClient(object): def _v1_auth(self, url): if self.proxy_token: - raise exceptions.NoTokenLookupException() + raise exceptions.AuthorizationFailure( + "This form of authentication does not support looking up" + " endpoints from an existing token.") headers = {'X-Auth-User': self.user, 'X-Auth-Key': self.password} @@ -339,7 +343,7 @@ class HTTPClient(object): elif resp.status_code == 305: return resp.headers['location'] else: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, "GET", url) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" @@ -369,14 +373,13 @@ class HTTPClient(object): token_url = url + "/tokens" # Make sure we follow redirects when trying to reach Keystone - resp, body = self.request( + return self._extract_service_catalog( + url, token_url, "POST", body=body, allow_redirects=True) - return self._extract_service_catalog(url, resp, body) - def get_volume_api_version_from_endpoint(self): magic_tuple = urlparse.urlsplit(self.management_url) scheme, netloc, path, query, frag = magic_tuple diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index 0c52c9c..e5e8678 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -1,156 +1,6 @@ -# Copyright 2010 Jacob Kaplan-Moss """ -Exception definitions. +Backwards compatible exceptions module. """ - -class UnsupportedVersion(Exception): - """Indicates that the user is trying to use an unsupported - version of the API. - """ - pass - - -class InvalidAPIVersion(Exception): - pass - - -class CommandError(Exception): - pass - - -class AuthorizationFailure(Exception): - pass - - -class NoUniqueMatch(Exception): - pass - - -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token. - """ - pass - - -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - - -class ClientException(Exception): - """ - The base exception class for all exceptions this library raises. - """ - def __init__(self, code, message=None, details=None, request_id=None): - self.code = code - self.message = message or self.__class__.message - self.details = details - self.request_id = request_id - - def __str__(self): - formatted_string = "%s (HTTP %s)" % (self.message, self.code) - if self.request_id: - formatted_string += " (Request-ID: %s)" % self.request_id - - return formatted_string - - -class BadRequest(ClientException): - """ - HTTP 400 - Bad request: you sent some malformed data. - """ - http_status = 400 - message = "Bad request" - - -class Unauthorized(ClientException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - http_status = 401 - message = "Unauthorized" - - -class Forbidden(ClientException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(ClientException): - """ - HTTP 404 - Not found - """ - http_status = 404 - message = "Not found" - - -class OverLimit(ClientException): - """ - HTTP 413 - Over limit: you're over the API limits for this time period. - """ - http_status = 413 - message = "Over limit" - - -# NotImplemented is a python keyword. -class HTTPNotImplemented(ClientException): - """ - HTTP 501 - Not Implemented: the server does not support this operation. - """ - http_status = 501 - message = "Not Implemented" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in ClientException.__subclasses__()) -# -# Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, - Forbidden, NotFound, - OverLimit, HTTPNotImplemented]) - - -def from_response(response, body): - """ - Return an instance of an ClientException or subclass - based on an requests response. - - Usage:: - - resp, body = requests.request(...) - if resp.status_code != 200: - raise exception_from_response(resp, rest.text) - """ - cls = _code_map.get(response.status_code, ClientException) - if response.headers: - request_id = response.headers.get('x-compute-request-id') - else: - request_id = None - if body: - message = "n/a" - details = "n/a" - if hasattr(body, 'keys'): - error = body[list(body.keys())[0]] - message = error.get('message', None) - details = error.get('details', None) - return cls(code=response.status_code, message=message, details=details, - request_id=request_id) - else: - return cls(code=response.status_code, request_id=request_id) +# flake8: noqa +from cinderclient.openstack.common.apiclient.exceptions import * diff --git a/cinderclient/openstack/common/apiclient/__init__.py b/cinderclient/openstack/common/apiclient/__init__.py new file mode 100644 index 0000000..d5d0022 --- /dev/null +++ b/cinderclient/openstack/common/apiclient/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# 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. diff --git a/cinderclient/openstack/common/apiclient/exceptions.py b/cinderclient/openstack/common/apiclient/exceptions.py new file mode 100644 index 0000000..e70d37a --- /dev/null +++ b/cinderclient/openstack/common/apiclient/exceptions.py @@ -0,0 +1,446 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 Nebula, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 OpenStack Foundation +# 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. + +""" +Exception definitions. +""" + +import sys + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises. + """ + pass + + +class MissingArgs(ClientException): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = "Missing argument(s): %s" % ", ".join(missing) + super(MissingArgs, self).__init__(msg) + + +class ValidationError(ClientException): + """Error in validation on API client side.""" + pass + + +class UnsupportedVersion(ClientException): + """User is trying to use an unsupported version of the API.""" + pass + + +class CommandError(ClientException): + """Error in CLI tool.""" + pass + + +class AuthorizationFailure(ClientException): + """Cannot authorize API client.""" + pass + + +class AuthPluginOptionsMissing(AuthorizationFailure): + """Auth plugin misses some options.""" + def __init__(self, opt_names): + super(AuthPluginOptionsMissing, self).__init__( + "Authentication failed. Missing options: %s" % + ", ".join(opt_names)) + self.opt_names = opt_names + + +class AuthSystemNotFound(AuthorizationFailure): + """User has specified a AuthSystem that is not installed.""" + def __init__(self, auth_system): + super(AuthSystemNotFound, self).__init__( + "AuthSystemNotFound: %s" % repr(auth_system)) + self.auth_system = auth_system + + +class NoUniqueMatch(ClientException): + """Multiple entities found instead of one.""" + pass + + +class EndpointException(ClientException): + """Something is rotten in Service Catalog.""" + pass + + +class EndpointNotFound(EndpointException): + """Could not find requested endpoint in Service Catalog.""" + pass + + +class AmbiguousEndpoints(EndpointException): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + super(AmbiguousEndpoints, self).__init__( + "AmbiguousEndpoints: %s" % repr(endpoints)) + self.endpoints = endpoints + + +class HttpError(ClientException): + """The base exception class for all HTTP exceptions. + """ + http_status = 0 + message = "HTTP Error" + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, http_status=None): + self.http_status = http_status or self.http_status + self.message = message or self.message + self.details = details + self.request_id = request_id + self.response = response + self.url = url + self.method = method + formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) + if request_id: + formatted_string += " (Request-ID: %s)" % request_id + super(HttpError, self).__init__(formatted_string) + + +class HttpClientError(HttpError): + """Client-side HTTP error. + + Exception for cases in which the client seems to have erred. + """ + message = "HTTP Client Error" + + +class HttpServerError(HttpError): + """Server-side HTTP error. + + Exception for cases in which the server is aware that it has + erred or is incapable of performing the request. + """ + message = "HTTP Server Error" + + +class BadRequest(HttpClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + http_status = 400 + message = "Bad Request" + + +class Unauthorized(HttpClientError): + """HTTP 401 - Unauthorized. + + Similar to 403 Forbidden, but specifically for use when authentication + is required and has failed or has not yet been provided. + """ + http_status = 401 + message = "Unauthorized" + + +class PaymentRequired(HttpClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + http_status = 402 + message = "Payment Required" + + +class Forbidden(HttpClientError): + """HTTP 403 - Forbidden. + + The request was a valid request, but the server is refusing to respond + to it. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(HttpClientError): + """HTTP 404 - Not Found. + + The requested resource could not be found but may be available again + in the future. + """ + http_status = 404 + message = "Not Found" + + +class MethodNotAllowed(HttpClientError): + """HTTP 405 - Method Not Allowed. + + A request was made of a resource using a request method not supported + by that resource. + """ + http_status = 405 + message = "Method Not Allowed" + + +class NotAcceptable(HttpClientError): + """HTTP 406 - Not Acceptable. + + The requested resource is only capable of generating content not + acceptable according to the Accept headers sent in the request. + """ + http_status = 406 + message = "Not Acceptable" + + +class ProxyAuthenticationRequired(HttpClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + http_status = 407 + message = "Proxy Authentication Required" + + +class RequestTimeout(HttpClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + http_status = 408 + message = "Request Timeout" + + +class Conflict(HttpClientError): + """HTTP 409 - Conflict. + + Indicates that the request could not be processed because of conflict + in the request, such as an edit conflict. + """ + http_status = 409 + message = "Conflict" + + +class Gone(HttpClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + http_status = 410 + message = "Gone" + + +class LengthRequired(HttpClientError): + """HTTP 411 - Length Required. + + The request did not specify the length of its content, which is + required by the requested resource. + """ + http_status = 411 + message = "Length Required" + + +class PreconditionFailed(HttpClientError): + """HTTP 412 - Precondition Failed. + + The server does not meet one of the preconditions that the requester + put on the request. + """ + http_status = 412 + message = "Precondition Failed" + + +class RequestEntityTooLarge(HttpClientError): + """HTTP 413 - Request Entity Too Large. + + The request is larger than the server is willing or able to process. + """ + http_status = 413 + message = "Request Entity Too Large" + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RequestEntityTooLarge, self).__init__(*args, **kwargs) + + +class RequestUriTooLong(HttpClientError): + """HTTP 414 - Request-URI Too Long. + + The URI provided was too long for the server to process. + """ + http_status = 414 + message = "Request-URI Too Long" + + +class UnsupportedMediaType(HttpClientError): + """HTTP 415 - Unsupported Media Type. + + The request entity has a media type which the server or resource does + not support. + """ + http_status = 415 + message = "Unsupported Media Type" + + +class RequestedRangeNotSatisfiable(HttpClientError): + """HTTP 416 - Requested Range Not Satisfiable. + + The client has asked for a portion of the file, but the server cannot + supply that portion. + """ + http_status = 416 + message = "Requested Range Not Satisfiable" + + +class ExpectationFailed(HttpClientError): + """HTTP 417 - Expectation Failed. + + The server cannot meet the requirements of the Expect request-header field. + """ + http_status = 417 + message = "Expectation Failed" + + +class UnprocessableEntity(HttpClientError): + """HTTP 422 - Unprocessable Entity. + + The request was well-formed but was unable to be followed due to semantic + errors. + """ + http_status = 422 + message = "Unprocessable Entity" + + +class InternalServerError(HttpServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + http_status = 500 + message = "Internal Server Error" + + +# NotImplemented is a python keyword. +class HttpNotImplemented(HttpServerError): + """HTTP 501 - Not Implemented. + + The server either does not recognize the request method, or it lacks + the ability to fulfill the request. + """ + http_status = 501 + message = "Not Implemented" + + +class BadGateway(HttpServerError): + """HTTP 502 - Bad Gateway. + + The server was acting as a gateway or proxy and received an invalid + response from the upstream server. + """ + http_status = 502 + message = "Bad Gateway" + + +class ServiceUnavailable(HttpServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + http_status = 503 + message = "Service Unavailable" + + +class GatewayTimeout(HttpServerError): + """HTTP 504 - Gateway Timeout. + + The server was acting as a gateway or proxy and did not receive a timely + response from the upstream server. + """ + http_status = 504 + message = "Gateway Timeout" + + +class HttpVersionNotSupported(HttpServerError): + """HTTP 505 - HttpVersion Not Supported. + + The server does not support the HTTP protocol version used in the request. + """ + http_status = 505 + message = "HTTP Version Not Supported" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in HttpError.__subclasses__()) +_code_map = {} +for obj in sys.modules[__name__].__dict__.values(): + if isinstance(obj, type): + try: + http_status = obj.http_status + except AttributeError: + pass + else: + if http_status: + _code_map[http_status] = obj + + +def from_response(response, method, url): + """Returns an instance of :class:`HttpError` or subclass based on response. + + :param response: instance of `requests.Response` class + :param method: HTTP method used for request + :param url: URL used for request + """ + kwargs = { + "http_status": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": response.headers.get("x-compute-request-id"), + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if hasattr(body, "keys"): + error = body[body.keys()[0]] + kwargs["message"] = error.get("message", None) + kwargs["details"] = error.get("details", None) + elif content_type.startswith("text/"): + kwargs["details"] = response.text + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HttpServerError + elif 400 <= response.status_code < 500: + cls = HttpClientError + else: + cls = HttpError + return cls(**kwargs) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index d067996..c5b1dd2 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -32,8 +32,8 @@ import logging import six from cinderclient import client -from cinderclient import exceptions as exc import cinderclient.extension +from cinderclient.openstack.common.apiclient import exceptions as exc from cinderclient.openstack.common import strutils from cinderclient import utils from cinderclient.v1 import shell as shell_v1 diff --git a/cinderclient/tests/test_base.py b/cinderclient/tests/test_base.py index 75c37e6..d22e2cd 100644 --- a/cinderclient/tests/test_base.py +++ b/cinderclient/tests/test_base.py @@ -1,5 +1,5 @@ from cinderclient import base -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient.v1 import volumes from cinderclient.tests import utils from cinderclient.tests.v1 import fakes diff --git a/cinderclient/tests/test_http.py b/cinderclient/tests/test_http.py index 3b93a4b..8c8c99d 100644 --- a/cinderclient/tests/test_http.py +++ b/cinderclient/tests/test_http.py @@ -3,7 +3,7 @@ import mock import requests from cinderclient import client -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient.tests import utils diff --git a/cinderclient/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py index c9d9819..6db8966 100644 --- a/cinderclient/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -1,4 +1,4 @@ -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import service_catalog from cinderclient.tests import utils diff --git a/cinderclient/tests/test_shell.py b/cinderclient/tests/test_shell.py index d6ef425..b301c21 100644 --- a/cinderclient/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -5,7 +5,7 @@ import fixtures from six import moves from testtools import matchers -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions import cinderclient.shell from cinderclient.tests import utils diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index 8df482d..aec1587 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -3,7 +3,7 @@ import sys from six import moves -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import utils from cinderclient import base from cinderclient.tests import utils as test_utils diff --git a/cinderclient/tests/utils.py b/cinderclient/tests/utils.py index 0ab8737..b258d80 100644 --- a/cinderclient/tests/utils.py +++ b/cinderclient/tests/utils.py @@ -29,10 +29,11 @@ class TestResponse(requests.Response): def __init__(self, data): self._text = None + self.headers = {} super(TestResponse, self) if isinstance(data, dict): self.status_code = data.get('status_code', None) - self.headers = data.get('headers', None) + self.headers = data.get('headers') or {} # Fake the text attribute to streamline Response creation self._text = data.get('text', None) else: diff --git a/cinderclient/tests/v1/test_auth.py b/cinderclient/tests/v1/test_auth.py index 704eacc..f87e6db 100644 --- a/cinderclient/tests/v1/test_auth.py +++ b/cinderclient/tests/v1/test_auth.py @@ -4,7 +4,7 @@ import mock import requests from cinderclient.v1 import client -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient.tests import utils diff --git a/cinderclient/tests/v2/test_auth.py b/cinderclient/tests/v2/test_auth.py index 89dd18f..d4d0b19 100644 --- a/cinderclient/tests/v2/test_auth.py +++ b/cinderclient/tests/v2/test_auth.py @@ -19,7 +19,7 @@ import json import mock import requests -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient.v2 import client from cinderclient.tests import utils diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 922f053..90dadd3 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -23,7 +23,7 @@ import uuid import six import prettytable -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient.openstack.common import strutils diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index f42d74e..38ab75e 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -22,7 +22,7 @@ import os import sys import time -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import utils diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index b86f7f9..9847c09 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -22,7 +22,7 @@ import time import six -from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import exceptions from cinderclient import utils diff --git a/openstack-common.conf b/openstack-common.conf index 35e0ccf..ce83deb 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=strutils +modules=apiclient,strutils # The base module to hold the copy of openstack.common base=cinderclient From 10df5beb397dee9f0bde20b87aa23035cc53ede7 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Mon, 10 Jun 2013 14:52:57 -0600 Subject: [PATCH 33/51] Implement ability to extend volume. Implements ability to call extend volume in Version 2 API. This change includes the needed support in cinderlcient to utilize/access changes made in the volume-resize change. Change-Id: Ifc7e2969b885a60e8105b5f3908ac452d0a61fa7 --- cinderclient/tests/v2/fakes.py | 6 ++++++ cinderclient/tests/v2/test_volumes.py | 5 +++++ cinderclient/v2/shell.py | 12 ++++++++++++ cinderclient/v2/volumes.py | 13 +++++++++++++ 4 files changed, 36 insertions(+) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index e888a8b..dd2b8fe 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -159,6 +159,10 @@ def _stub_transfer(id, base_uri, tenant_id): } +def _stub_extend(id, new_size): + return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -290,6 +294,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert body[action] is None elif action == 'os-reset_status': assert 'status' in body[action] + elif action == 'os-extend': + assert body[action].keys() == ['new_size'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index a66dd8c..8a2560d 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -85,3 +85,8 @@ class VolumesTest(utils.TestCase): keys = ['key1'] cs.volumes.delete_metadata(1234, keys) cs.assert_called('DELETE', '/volumes/1234/metadata/key1') + + def test_extend(self): + v = cs.volumes.get('1234') + cs.volumes.extend(v, 2) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index b86f7f9..835c8ab 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -900,3 +900,15 @@ def do_transfer_show(cs, args): info.pop('links') utils.print_dict(info) + + +@utils.arg('volume', metavar='', help='ID of the volume to extend.') +@utils.arg('new-size', + metavar='', + type=int, + help='New size of volume in GB') +@utils.service_type('volume') +def do_extend(cs, args): + """Attempt to extend the size of an existing volume.""" + volume = _find_volume(cs, args.volume) + cs.volumes.extend_volume(volume, args.new_size) diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 3982e4e..273668f 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -104,6 +104,14 @@ class Volume(base.Resource): """Update the volume with the provided state.""" self.manager.reset_state(self, state) + def extend(self, volume, new_size): + """Extend the size of the specified volume. + :param volume: The UUID of the volume to extend + :param new_size: The desired size to extend volume to. + """ + + self.manager.extend(self, volume, new_size) + class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" @@ -321,3 +329,8 @@ class VolumeManager(base.ManagerWithFind): def reset_state(self, volume, state): """Update the provided volume with the provided state.""" return self._action('os-reset_status', volume, {'status': state}) + + def extend(self, volume, new_size): + return self._action('os-extend', + base.getid(volume), + {'new_size': new_size}) From ee9f0354ec7c0084c0f8cfe2b670589e0d326421 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Mon, 1 Jul 2013 21:17:30 -0600 Subject: [PATCH 34/51] Enable ability to reset state on snapshots. Implement the ability to reset-state on snapshots, exposes os-reset_status from snapshot actions. Change-Id: I605f41e39fde46ccfe6a53222b798f32c14bc1f8 --- cinderclient/tests/v1/fakes.py | 11 +++++++++++ cinderclient/tests/v1/test_shell.py | 10 ++++++++++ cinderclient/tests/v2/fakes.py | 11 +++++++++++ cinderclient/tests/v2/test_shell.py | 10 ++++++++++ cinderclient/v1/shell.py | 15 +++++++++++++++ cinderclient/v1/volume_snapshots.py | 15 +++++++++++++++ cinderclient/v2/shell.py | 15 +++++++++++++++ cinderclient/v2/volume_snapshots.py | 15 +++++++++++++++ 8 files changed, 102 insertions(+) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index a223d16..2a33ba8 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -231,6 +231,17 @@ class FakeHTTPClient(base_client.HTTPClient): snapshot.update(kw['body']['snapshot']) return (200, {}, {'snapshot': snapshot}) + def post_snapshots_1234_action(self, body, **kw): + _body = None + resp = 202 + assert len(body.keys()) == 1 + action = body.keys()[0] + if action == 'os-reset_status': + assert 'status' in body['os-reset_status'] + else: + raise AssertionError('Unexpected action: %s" % action') + return (resp, {}, _body) + # # Volumes # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 8b23d6f..a8c9425 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -189,3 +189,13 @@ class ShellTest(utils.TestCase): self.run_command('reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_snapshot_reset_state(self): + self.run_command('snapshot-reset-state 1234') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called('POST', '/snapshots/1234/action', body=expected) + + def test_snapshot_reset_state_with_flag(self): + self.run_command('snapshot-reset-state --state error 1234') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called('POST', '/snapshots/1234/action', body=expected) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index e888a8b..c1b5be5 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -238,6 +238,17 @@ class FakeHTTPClient(base_client.HTTPClient): snapshot.update(kw['body']['snapshot']) return (200, {}, {'snapshot': snapshot}) + def post_snapshots_1234_action(self, body, **kw): + _body = None + resp = 202 + assert len(body.keys()) == 1 + action = body.keys()[0] + if action == 'os-reset_status': + assert 'status' in body['os-reset_status'] + else: + raise AssertionError('Unexpected action: %s" % action') + return (resp, {}, _body) + # # Volumes # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index ce7842c..eabf134 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -167,3 +167,13 @@ class ShellTest(utils.TestCase): self.run_command('reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_snapshot_reset_state(self): + self.run_command('snapshot-reset-state 1234') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called('POST', '/snapshots/1234/action', body=expected) + + def test_snapshot_reset_state_with_flag(self): + self.run_command('snapshot-reset-state --state error 1234') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called('POST', '/snapshots/1234/action', body=expected) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index f42d74e..684728b 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -443,6 +443,21 @@ def do_snapshot_rename(cs, args): _find_volume_snapshot(cs, args.snapshot).update(**kwargs) +@utils.arg('snapshot', metavar='', + help='ID of the snapshot to modify.') +@utils.arg('--state', metavar='', + default='available', + help=('Indicate which state to assign the snapshot. ' + 'Options include available, error, creating, deleting, ' + 'error_deleting. If no state is provided, ' + 'available will be used.')) +@utils.service_type('volume') +def do_snapshot_reset_state(cs, args): + """Explicitly update the state of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + snapshot.reset_state(args.state) + + def _print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name']) diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index 66237a5..63e92c8 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -53,6 +53,10 @@ class Snapshot(base.Resource): def project_id(self): return self._info.get('os-extended-snapshot-attributes:project_id') + def reset_state(self, state): + """Update the snapshot with the privided state.""" + self.manager.reset_state(self, state) + class SnapshotManager(base.ManagerWithFind): """ @@ -133,3 +137,14 @@ class SnapshotManager(base.ManagerWithFind): body = {"snapshot": kwargs} self._update("/snapshots/%s" % base.getid(snapshot), body) + + def reset_state(self, snapshot, state): + """Update the specified volume with the provided state.""" + return self._action('os-reset_status', snapshot, {'status': state}) + + def _action(self, action, snapshot, info=None, **kwargs): + """Perform a snapshot action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/snapshots/%s/action' % base.getid(snapshot) + return self.api.client.post(url, body=body) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index b86f7f9..70b259a 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -512,6 +512,21 @@ def do_snapshot_rename(cs, args): _find_volume_snapshot(cs, args.snapshot).update(**kwargs) +@utils.arg('snapshot', metavar='', + help='ID of the snapshot to modify.') +@utils.arg('--state', metavar='', + default='available', + help=('Indicate which state to assign the snapshot. ' + 'Options include available, error, creating, ' + 'deleting, error_deleting. If no state is provided, ' + 'available will be used.')) +@utils.service_type('snapshot') +def do_snapshot_reset_state(cs, args): + """Explicitly update the state of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + snapshot.reset_state(args.state) + + def _print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name']) diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index b16383c..ef529eb 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -45,6 +45,10 @@ class Snapshot(base.Resource): def project_id(self): return self._info.get('os-extended-snapshot-attributes:project_id') + def reset_state(self, state): + """Update the snapshot with the provided state.""" + self.manager.reset_state(self, state) + class SnapshotManager(base.ManagerWithFind): """Manage :class:`Snapshot` resources.""" @@ -118,3 +122,14 @@ class SnapshotManager(base.ManagerWithFind): body = {"snapshot": kwargs} self._update("/snapshots/%s" % base.getid(snapshot), body) + + def reset_state(self, snapshot, state): + """Update the specified snapshot with the provided state.""" + return self._action('os-reset_status', snapshot, {'status': state}) + + def _action(self, action, snapshot, info=None, **kwargs): + """Perform a snapshot action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/snapshots/%s/action' % base.getid(snapshot) + return self.api.client.post(url, body=body) From a26044f6d700868b4715cf0e8e20ce380acab8db Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jul 2013 22:52:52 -0400 Subject: [PATCH 35/51] Sync install_venv_common from oslo Change-Id: I1649a8e777baf288b3aa4c2c29e2fe532dfe93be --- openstack-common.conf | 4 +- tools/install_venv.py | 268 +++++++---------------------------- tools/install_venv_common.py | 212 +++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 220 deletions(-) create mode 100644 tools/install_venv_common.py diff --git a/openstack-common.conf b/openstack-common.conf index ce83deb..4ee0ff3 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,9 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=apiclient,strutils +module=apiclient +module=strutils +module=install_venv_common # The base module to hold the copy of openstack.common base=cinderclient diff --git a/tools/install_venv.py b/tools/install_venv.py index 55603d2..0011a8b 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -4,244 +4,74 @@ # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # -# Copyright 2010 OpenStack, LLC +# Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # -# 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 +# 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 +# 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. +# 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. -""" -Installation script for Nova's development virtualenv -""" - -from __future__ import print_function - -import optparse +import ConfigParser import os -import subprocess import sys -import platform + +import install_venv_common as install_venv # flake8: noqa -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt') -TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt') -PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - - -def die(message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - -def check_python_version(): - if sys.version_info < (2, 6): - die("Need Python Version >= 2.6") - - -def run_command_with_code(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - return run_command_with_code(cmd, redirect_output, check_exit_code)[0] - - -class Distro(object): - - def check_cmd(self, cmd): - return bool(run_command(['which', cmd], check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') - if run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - die('ERROR: virtualenv not found.\n\nDevelopment' - ' requires virtualenv, please install it using your' - ' favorite package management tool') - - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv.""" - pass - - -class Debian(Distro): - """This covers all Debian-based distributions.""" - - def check_pkg(self, pkg): - return run_command_with_code(['dpkg', '-l', pkg], - check_exit_code=False)[1] == 0 - - def apt_install(self, pkg, **kwargs): - run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.apt_install('python-virtualenv', check_exit_code=False) - - super(Debian, self).install_virtualenv() - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux""" - - def check_pkg(self, pkg): - return run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def yum_install(self, pkg, **kwargs): - run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.yum_install('python-virtualenv', check_exit_code=False) - - super(Fedora, self).install_virtualenv() - - -def get_distro(): - if os.path.exists('/etc/fedora-release') or \ - os.path.exists('/etc/redhat-release'): - return Fedora() - elif os.path.exists('/etc/debian_version'): - return Debian() - else: - return Distro() - - -def check_dependencies(): - get_distro().install_virtualenv() - - -def create_virtualenv(venv=VENV, no_site_packages=True): - """Creates the virtual environment and installs PIP only into the - virtual environment - """ - print('Creating venv...', end=' ') - if no_site_packages: - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - else: - run_command(['virtualenv', '-q', VENV]) - print('done.') - print('Installing pip in virtualenv...', end=' ') - if not run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - die("Failed to install pip.") - print('done.') - - -def pip_install(*args): - run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - -def install_dependencies(venv=VENV): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and distribute. - pip_install('pip') - pip_install('distribute') - - pip_install('-r', PIP_REQUIRES) - pip_install('-r', TEST_REQUIRES) - # Tell the virtual env how to "import cinder" - pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages", - "cinderclient.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def post_process(): - get_distro().post_process() - - -def print_help(): +def print_help(project, venv, root): help = """ - python-cinderclient development environment setup is complete. + %(project)s development environment setup is complete. - python-cinderclient development uses virtualenv to track and manage Python + %(project)s development uses virtualenv to track and manage Python dependencies while in development and testing. - To activate the python-cinderclient virtualenv for the extent of your - current shell session you can run: + To activate the %(project)s virtualenv for the extent of your current + shell session you can run: - $ source .venv/bin/activate + $ source %(venv)s/bin/activate - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: + Or, if you prefer, you can run commands in the virtualenv on a case by + case basis by running: - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. + $ %(root)s/tools/with_venv.sh """ - print(help) - - -def parse_args(): - """Parse command-line arguments""" - parser = optparse.OptionParser() - parser.add_option("-n", "--no-site-packages", dest="no_site_packages", - default=False, action="store_true", - help="Do not inherit packages from global Python install") - return parser.parse_args() + print help % dict(project=project, venv=venv, root=root) def main(argv): - (options, args) = parse_args() - check_python_version() - check_dependencies() - create_virtualenv(no_site_packages=options.no_site_packages) - install_dependencies() - post_process() - print_help() + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + setup_cfg = ConfigParser.ConfigParser() + setup_cfg.read('setup.cfg') + project = setup_cfg.get('metadata', 'name') + + install = install_venv.InstallVenv( + root, venv, pip_requires, test_requires, py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + install.post_process() + print_help(project, venv, root) if __name__ == '__main__': main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py new file mode 100644 index 0000000..f428c1e --- /dev/null +++ b/tools/install_venv_common.py @@ -0,0 +1,212 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# Copyright 2013 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. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools. + self.pip_install('pip>=1.3') + self.pip_install('setuptools') + + self.pip_install('-r', self.requirements) + self.pip_install('-r', self.test_requirements) + + def post_process(self): + self.get_distro().post_process() + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + def post_process(self): + """Any distribution-specific post-processing gets done here. + + In particular, this is useful for applying patches to code inside + the venv. + """ + pass + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def apply_patch(self, originalfile, patchfile): + self.run_command(['patch', '-N', originalfile, patchfile], + check_exit_code=False) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() + + def post_process(self): + """Workaround for a bug in eventlet. + + This currently affects RHEL6.1, but the fix can safely be + applied to all RHEL and Fedora distributions. + + This can be removed when the fix is applied upstream. + + Nova: https://bugs.launchpad.net/nova/+bug/884915 + Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 + RHEL: https://bugzilla.redhat.com/958868 + """ + + # Install "patch" program if it's not there + if not self.check_pkg('patch'): + self.die("Please install 'patch'.") + + # Apply the eventlet patch + self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, + 'site-packages', + 'eventlet/green/subprocess.py'), + 'contrib/redhat-eventlet.patch') From d702d5443f899d0011532cf13320a793e396983b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Mon, 8 Jul 2013 17:59:59 -0400 Subject: [PATCH 36/51] Implement ability to extend volume for v1. Backport implementation from v2 to v1. Change-Id: Ibb2334859e937a37481308580b072daef068f511 --- cinderclient/tests/v1/fakes.py | 6 ++++++ cinderclient/tests/v1/test_volumes.py | 5 +++++ cinderclient/v1/shell.py | 12 ++++++++++++ cinderclient/v1/volumes.py | 14 ++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 2a33ba8..56f086b 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -152,6 +152,10 @@ def _stub_transfer(id, base_uri, tenant_id): } +def _stub_extend(id, new_size): + return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -294,6 +298,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert body[action] is None elif action == 'os-reset_status': assert 'status' in body[action] + elif action == 'os-extend': + assert body[action].keys() == ['new_size'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index 16410c9..768e942 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -69,3 +69,8 @@ class VolumesTest(utils.TestCase): keys = ['key1'] cs.volumes.delete_metadata(1234, keys) cs.assert_called('DELETE', '/volumes/1234/metadata/key1') + + def test_extend(self): + v = cs.volumes.get('1234') + cs.volumes.extend(v, 2) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index f3ac3d9..eeb30ba 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -819,3 +819,15 @@ def do_transfer_show(cs, args): info.pop('links') utils.print_dict(info) + + +@utils.arg('volume', metavar='', help='ID of the volume to extend.') +@utils.arg('new_size', + metavar='', + type=int, + help='New size of volume in GB') +@utils.service_type('volume') +def do_extend(cs, args): + """Attempt to extend the size of an existing volume.""" + volume = _find_volume(cs, args.volume) + cs.volumes.extend(volume, args.new_size) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 066890d..5f6c566 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -105,6 +105,15 @@ class Volume(base.Resource): """Update the volume with the provided state.""" self.manager.reset_state(self, state) + def extend(self, volume, new_size): + """Extend the size of the specified volume. + + :param volume: The UUID of the volume to extend + :param new_size: The desired size to extend volume to. + """ + + self.manager.extend(self, volume, new_size) + class VolumeManager(base.ManagerWithFind): """ @@ -338,3 +347,8 @@ class VolumeManager(base.ManagerWithFind): def reset_state(self, volume, state): """Update the provided volume with the provided state.""" return self._action('os-reset_status', volume, {'status': state}) + + def extend(self, volume, new_size): + return self._action('os-extend', + base.getid(volume), + {'new_size': new_size}) From 1a15caff9481e230ba7c47d9c1f33a99f423e15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Mon, 8 Jul 2013 19:19:10 -0400 Subject: [PATCH 37/51] Fix wrong method call for extend subcommand Ensure extend is called instead of extend_volume as the latter doesn't exist. Change-Id: I5a4ce5904dc73586a124f7bddebc8ffd30841980 --- cinderclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6962ed9..98883d6 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -915,4 +915,4 @@ def do_transfer_show(cs, args): def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" volume = _find_volume(cs, args.volume) - cs.volumes.extend_volume(volume, args.new_size) + cs.volumes.extend(volume, args.new_size) From 2b0f086e7b4db4daa6e096eeb2adc96b1d4884e3 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sat, 13 Jul 2013 14:46:57 -0600 Subject: [PATCH 38/51] Update index.rst Update the release info in index.rst to get ready to push new version to PyPi. Change-Id: Id3b4b6bfd7374b74bf8566dc133ea66f5d2f74ed --- doc/source/index.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index e82c824..784a665 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -27,6 +27,23 @@ Once you've configured your authentication parameters, you can run ``cinder help Release Notes ============= +1.0.5 +----- +* Add support for scheduler-hints +* Add support to extend volumes +* Add support to reset state on volumes and snapshots +* Add snapshot support for quota class + +.. _1190853: http://bugs.launchpad.net/python-cinderclient/+bug/1190853 +.. _1190731: http://bugs.launchpad.net/python-cinderclient/+bug/1190731 +.. _1169455: http://bugs.launchpad.net/python-cinderclient/+bug/1169455 +.. _1188452: http://bugs.launchpad.net/python-cinderclient/+bug/1188452 +.. _1180393: http://bugs.launchpad.net/python-cinderclient/+bug/1180393 +.. _1182678: http://bugs.launchpad.net/python-cinderclient/+bug/1182678 +.. _1179008: http://bugs.launchpad.net/python-cinderclient/+bug/1179008 +.. _1180059: http://bugs.launchpad.net/python-cinderclient/+bug/1180059 +.. _1170565: http://bugs.launchpad.net/python-cinderclient/+bug/1170565 + 1.0.4 ----- * Added suport for backup-service commands From 627b616227badd893ff2d8d7addf162d605b2299 Mon Sep 17 00:00:00 2001 From: Qiu Yu Date: Sun, 14 Jul 2013 23:18:22 +0800 Subject: [PATCH 39/51] Add os-services extension support Implement client bindings for Cinder os-services API extension, so client would be able to list services, enable or disable particular services. Usage: cinder service-list [--host ] [--binary ] cinder service-enable cinder service-disable This change is depended on following change at Cinder side I7f3fa889294ca6caebdf46b8689345bcac1cdf54 Implements blueprint os-services-extension Change-Id: I4a53fd545ed3b446441302d00a429168a996a34a --- cinderclient/tests/v1/fakes.py | 48 ++++++++++++++++++++ cinderclient/tests/v1/test_services.py | 62 ++++++++++++++++++++++++++ cinderclient/tests/v2/fakes.py | 48 ++++++++++++++++++++ cinderclient/tests/v2/test_services.py | 62 ++++++++++++++++++++++++++ cinderclient/v1/client.py | 2 + cinderclient/v1/services.py | 56 +++++++++++++++++++++++ cinderclient/v1/shell.py | 28 ++++++++++++ cinderclient/v2/client.py | 2 + cinderclient/v2/services.py | 56 +++++++++++++++++++++++ cinderclient/v2/shell.py | 28 ++++++++++++ 10 files changed, 392 insertions(+) create mode 100644 cinderclient/tests/v1/test_services.py create mode 100644 cinderclient/tests/v2/test_services.py create mode 100644 cinderclient/v1/services.py create mode 100644 cinderclient/v2/services.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 56f086b..73ad1a9 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime + try: import urlparse except ImportError: @@ -510,3 +512,49 @@ class FakeHTTPClient(base_client.HTTPClient): transfer1 = '5678' return (200, {}, {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) + + # + # Services + # + def get_os_services(self, **kw): + host = kw.get('host', None) + binary = kw.get('binary', None) + services = [ + { + 'binary': 'cinder-volume', + 'host': 'host1', + 'zone': 'cinder', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime(2012, 10, 29, 13, 42, 2) + }, + { + 'binary': 'cinder-volume', + 'host': 'host2', + 'zone': 'cinder', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime(2012, 9, 18, 8, 3, 38) + }, + { + 'binary': 'cinder-scheduler', + 'host': 'host2', + 'zone': 'cinder', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime(2012, 9, 18, 8, 3, 38) + }, + ] + if host: + services = filter(lambda i: i['host'] == host, services) + if binary: + services = filter(lambda i: i['binary'] == binary, services) + return (200, {}, {'services': services}) + + def put_os_services_enable(self, body, **kw): + return (200, {}, {'host': body['host'], 'binary': body['binary'], + 'status': 'disabled'}) + + def put_os_services_disable(self, body, **kw): + return (200, {}, {'host': body['host'], 'binary': body['binary'], + 'status': 'enabled'}) diff --git a/cinderclient/tests/v1/test_services.py b/cinderclient/tests/v1/test_services.py new file mode 100644 index 0000000..2320a26 --- /dev/null +++ b/cinderclient/tests/v1/test_services.py @@ -0,0 +1,62 @@ +# Copyright 2013 OpenStack LLC. +# 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 import utils +from cinderclient.tests.v1 import fakes +from cinderclient.v1 import services + + +cs = fakes.FakeClient() + + +class ServicesTest(utils.TestCase): + + def test_list_services(self): + svs = cs.services.list() + cs.assert_called('GET', '/os-services') + self.assertEqual(len(svs), 3) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + + def test_list_services_with_hostname(self): + svs = cs.services.list(host='host2') + cs.assert_called('GET', '/os-services?host=host2') + self.assertEqual(len(svs), 2) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + + def test_list_services_with_binary(self): + svs = cs.services.list(binary='cinder-volume') + cs.assert_called('GET', '/os-services?binary=cinder-volume') + self.assertEqual(len(svs), 2) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'cinder-volume') for s in svs] + + def test_list_services_with_host_binary(self): + svs = cs.services.list('host2', 'cinder-volume') + cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume') + self.assertEqual(len(svs), 1) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + [self.assertEqual(s.binary, 'cinder-volume') for s in svs] + + def test_services_enable(self): + cs.services.enable('host1', 'cinder-volume') + values = {"host": "host1", 'binary': 'cinder-volume'} + cs.assert_called('PUT', '/os-services/enable', values) + + def test_services_disable(self): + cs.services.disable('host1', 'cinder-volume') + values = {"host": "host1", 'binary': 'cinder-volume'} + cs.assert_called('PUT', '/os-services/disable', values) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index e46b822..a140082 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime + try: import urlparse except ImportError: @@ -517,3 +519,49 @@ class FakeHTTPClient(base_client.HTTPClient): transfer1 = '5678' return (200, {}, {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) + + # + # Services + # + def get_os_services(self, **kw): + host = kw.get('host', None) + binary = kw.get('binary', None) + services = [ + { + 'binary': 'cinder-volume', + 'host': 'host1', + 'zone': 'cinder', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime(2012, 10, 29, 13, 42, 2) + }, + { + 'binary': 'cinder-volume', + 'host': 'host2', + 'zone': 'cinder', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime(2012, 9, 18, 8, 3, 38) + }, + { + 'binary': 'cinder-scheduler', + 'host': 'host2', + 'zone': 'cinder', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime(2012, 9, 18, 8, 3, 38) + }, + ] + if host: + services = filter(lambda i: i['host'] == host, services) + if binary: + services = filter(lambda i: i['binary'] == binary, services) + return (200, {}, {'services': services}) + + def put_os_services_enable(self, body, **kw): + return (200, {}, {'host': body['host'], 'binary': body['binary'], + 'status': 'disabled'}) + + def put_os_services_disable(self, body, **kw): + return (200, {}, {'host': body['host'], 'binary': body['binary'], + 'status': 'enabled'}) diff --git a/cinderclient/tests/v2/test_services.py b/cinderclient/tests/v2/test_services.py new file mode 100644 index 0000000..e4bce29 --- /dev/null +++ b/cinderclient/tests/v2/test_services.py @@ -0,0 +1,62 @@ +# Copyright 2013 OpenStack LLC. +# 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 import utils +from cinderclient.tests.v2 import fakes +from cinderclient.v2 import services + + +cs = fakes.FakeClient() + + +class ServicesTest(utils.TestCase): + + def test_list_services(self): + svs = cs.services.list() + cs.assert_called('GET', '/os-services') + self.assertEqual(len(svs), 3) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + + def test_list_services_with_hostname(self): + svs = cs.services.list(host='host2') + cs.assert_called('GET', '/os-services?host=host2') + self.assertEqual(len(svs), 2) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + + def test_list_services_with_binary(self): + svs = cs.services.list(binary='cinder-volume') + cs.assert_called('GET', '/os-services?binary=cinder-volume') + self.assertEqual(len(svs), 2) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'cinder-volume') for s in svs] + + def test_list_services_with_host_binary(self): + svs = cs.services.list('host2', 'cinder-volume') + cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume') + self.assertEqual(len(svs), 1) + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + [self.assertEqual(s.binary, 'cinder-volume') for s in svs] + + def test_services_enable(self): + cs.services.enable('host1', 'cinder-volume') + values = {"host": "host1", 'binary': 'cinder-volume'} + cs.assert_called('PUT', '/os-services/enable', values) + + def test_services_disable(self): + cs.services.disable('host1', 'cinder-volume') + values = {"host": "host1", 'binary': 'cinder-volume'} + cs.assert_called('PUT', '/os-services/disable', values) diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 4f8b92d..7dd84e1 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -17,6 +17,7 @@ from cinderclient import client from cinderclient.v1 import limits from cinderclient.v1 import quota_classes from cinderclient.v1 import quotas +from cinderclient.v1 import services from cinderclient.v1 import volumes from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volume_types @@ -62,6 +63,7 @@ class Client(object): self.backups = volume_backups.VolumeBackupManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) + self.services = services.ServiceManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/services.py b/cinderclient/v1/services.py new file mode 100644 index 0000000..b2427dd --- /dev/null +++ b/cinderclient/v1/services.py @@ -0,0 +1,56 @@ +# Copyright 2013 OpenStack LLC. +# 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. + +""" +service interface +""" +from cinderclient import base + + +class Service(base.Resource): + + def __repr__(self): + return "" % self.service + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, binary=None): + """ + Describes service list for host. + + :param host: destination host name. + :param binary: service binary. + """ + url = "/os-services" + filters = [] + if host: + filters.append("host=%s" % host) + if binary: + filters.append("binary=%s" % binary) + if filters: + url = "%s?%s" % (url, "&".join(filters)) + return self._list(url, "services") + + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + self._update("/os-services/enable", body) + + def disable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + self._update("/os-services/disable", body) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index eeb30ba..af16ed0 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -831,3 +831,31 @@ def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" volume = _find_volume(cs, args.volume) cs.volumes.extend(volume, args.new_size) + + +@utils.arg('--host', metavar='', default=None, + help='Name of host.') +@utils.arg('--binary', metavar='', default=None, + help='Service binary.') +@utils.service_type('volume') +def do_service_list(cs, args): + """List all the services. Filter by host & service binary.""" + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.service_type('volume') +def do_service_enable(cs, args): + """Enable the service.""" + cs.services.enable(args.host, args.binary) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.service_type('volume') +def do_service_disable(cs, args): + """Disable the service.""" + cs.services.disable(args.host, args.binary) diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 261f848..003ecae 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -17,6 +17,7 @@ from cinderclient import client from cinderclient.v2 import limits from cinderclient.v2 import quota_classes from cinderclient.v2 import quotas +from cinderclient.v2 import services from cinderclient.v2 import volumes from cinderclient.v2 import volume_snapshots from cinderclient.v2 import volume_types @@ -60,6 +61,7 @@ class Client(object): self.backups = volume_backups.VolumeBackupManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) + self.services = services.ServiceManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v2/services.py b/cinderclient/v2/services.py new file mode 100644 index 0000000..b2427dd --- /dev/null +++ b/cinderclient/v2/services.py @@ -0,0 +1,56 @@ +# Copyright 2013 OpenStack LLC. +# 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. + +""" +service interface +""" +from cinderclient import base + + +class Service(base.Resource): + + def __repr__(self): + return "" % self.service + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, binary=None): + """ + Describes service list for host. + + :param host: destination host name. + :param binary: service binary. + """ + url = "/os-services" + filters = [] + if host: + filters.append("host=%s" % host) + if binary: + filters.append("binary=%s" % binary) + if filters: + url = "%s?%s" % (url, "&".join(filters)) + return self._list(url, "services") + + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + self._update("/os-services/enable", body) + + def disable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + self._update("/os-services/disable", body) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 98883d6..7f57358 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -916,3 +916,31 @@ def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" volume = _find_volume(cs, args.volume) cs.volumes.extend(volume, args.new_size) + + +@utils.arg('--host', metavar='', default=None, + help='Name of host.') +@utils.arg('--binary', metavar='', default=None, + help='Service binary.') +@utils.service_type('volume') +def do_service_list(cs, args): + """List all the services. Filter by host & service binary.""" + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.service_type('volume') +def do_service_enable(cs, args): + """Enable the service.""" + cs.services.enable(args.host, args.binary) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.service_type('volume') +def do_service_disable(cs, args): + """Disable the service.""" + cs.services.disable(args.host, args.binary) From fdbbb1076e2ce7c22bc71ba9b49020ce1e79603a Mon Sep 17 00:00:00 2001 From: Seif Lotfy Date: Sun, 14 Jul 2013 21:31:32 +0000 Subject: [PATCH 40/51] Add print to the upload-to-image command Change-Id: I4f7fa6f943fce8151ebd609f35c4717f868a5dfb --- cinderclient/v1/shell.py | 12 ++++++++---- cinderclient/v1/volumes.py | 4 ++-- cinderclient/v2/shell.py | 12 ++++++++---- cinderclient/v2/volumes.py | 4 ++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index eeb30ba..395cc72 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -86,6 +86,10 @@ def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) +def _print_volume_image(image): + utils.print_dict(image[1]['os-volume_upload_image']) + + def _translate_keys(collection, convert): for item in collection: keys = list(item.__dict__.keys()) @@ -683,10 +687,10 @@ def _find_volume_type(cs, vtype): def do_upload_to_image(cs, args): """Upload volume to image service as image.""" volume = _find_volume(cs, args.volume_id) - volume.upload_to_image(args.force, - args.image_name, - args.container_format, - args.disk_format) + _print_volume_image(volume.upload_to_image(args.force, + args.image_name, + args.container_format, + args.disk_format)) @utils.arg('volume', metavar='', diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 5f6c566..9c870cb 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -91,8 +91,8 @@ class Volume(base.Resource): def upload_to_image(self, force, image_name, container_format, disk_format): """Upload a volume to image service as an image.""" - self.manager.upload_to_image(self, force, image_name, container_format, - disk_format) + return self.manager.upload_to_image(self, force, image_name, + container_format, disk_format) def force_delete(self): """Delete the specified volume ignoring its current state. diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 98883d6..f400760 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -80,6 +80,10 @@ def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) +def _print_volume_image(image): + utils.print_dict(image[1]['os-volume_upload_image']) + + def _translate_keys(collection, convert): for item in collection: keys = list(item.__dict__.keys()) @@ -759,10 +763,10 @@ def _find_volume_type(cs, vtype): def do_upload_to_image(cs, args): """Upload volume to image service as image.""" volume = _find_volume(cs, args.volume_id) - volume.upload_to_image(args.force, - args.image_name, - args.container_format, - args.disk_format) + _print_volume_image(volume.upload_to_image(args.force, + args.image_name, + args.container_format, + args.disk_format)) @utils.arg('volume', metavar='', diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 273668f..14535af 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -90,8 +90,8 @@ class Volume(base.Resource): def upload_to_image(self, force, image_name, container_format, disk_format): """Upload a volume to image service as an image.""" - self.manager.upload_to_image(self, force, image_name, container_format, - disk_format) + return self.manager.upload_to_image(self, force, image_name, + container_format, disk_format) def force_delete(self): """Delete the specified volume ignoring its current state. From d7208847efb5bb0df1d1f3340681915d867847b9 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sun, 14 Jul 2013 19:47:58 -0600 Subject: [PATCH 41/51] Update to latest openstack/requirements. Fixes bug: 1200214 Change-Id: I0ff8a76eb5d5a99892a270909c68207858e1bc8b --- requirements.txt | 4 ++-- test-requirements.txt | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 27c12b9..ab80cff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ d2to1>=0.2.10,<0.3 -pbr>=0.5,<0.6 +pbr>=0.5.16,<0.6 argparse prettytable>=0.6,<0.8 -requests>=0.8 +requests>=1.1,<1.2.3 simplejson>=2.0.9 six diff --git a/test-requirements.txt b/test-requirements.txt index 34115a4..ff2d884 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,13 +2,12 @@ pep8==1.4.5 pyflakes==0.7.2 flake8==2.0 -hacking>=0.5.3,<0.6 - +hacking>=0.5.6,<0.6 coverage>=3.6 discover fixtures>=0.3.12 mock>=0.8.0 python-subunit sphinx>=1.1.2 -testtools>=0.9.29 -testrepository>=0.0.13 +testtools>=0.9.32 +testrepository>=0.0.15 From 784f53f0a0c685ffad54ec3adb4859de758c51ba Mon Sep 17 00:00:00 2001 From: Cory Stone Date: Thu, 21 Mar 2013 16:53:43 -0500 Subject: [PATCH 42/51] Changes for volume type quotas. blueprint quotas-limits-by-voltype Change-Id: I1bb676689c79fe1b14a14a81b21105a02ec117ef --- cinderclient/v1/quota_classes.py | 18 ++++-------------- cinderclient/v1/quotas.py | 14 ++++---------- cinderclient/v1/shell.py | 19 ++++++++++++++++++- cinderclient/v2/quota_classes.py | 18 ++++-------------- cinderclient/v2/quotas.py | 14 ++++---------- cinderclient/v2/shell.py | 19 ++++++++++++++++++- 6 files changed, 52 insertions(+), 50 deletions(-) diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index df637ea..c6a85f4 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -36,20 +36,10 @@ class QuotaClassSetManager(base.Manager): return self._get("/os-quota-class-sets/%s" % (class_name), "quota_class_set") - def update(self, - class_name, - volumes=None, - snapshots=None, - gigabytes=None): + def update(self, class_name, **updates): + body = {'quota_class_set': {'class_name': class_name}} - body = {'quota_class_set': { - 'class_name': class_name, - 'volumes': volumes, - 'snapshots': snapshots, - 'gigabytes': gigabytes}} - - for key in list(body['quota_class_set'].keys()): - if body['quota_class_set'][key] is None: - body['quota_class_set'].pop(key) + for update in updates.keys(): + body['quota_class_set'][update] = updates[update] self._update('/os-quota-class-sets/%s' % (class_name), body) diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index ce7a912..bf37462 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -37,17 +37,11 @@ class QuotaSetManager(base.Manager): tenant_id = tenant_id.tenant_id return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") - def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None): + def update(self, tenant_id, **updates): + body = {'quota_set': {'tenant_id': tenant_id}} - body = {'quota_set': { - 'tenant_id': tenant_id, - 'volumes': volumes, - 'snapshots': snapshots, - 'gigabytes': gigabytes}} - - for key in list(body['quota_set'].keys()): - if body['quota_set'][key] is None: - body['quota_set'].pop(key) + for update in updates.keys(): + body['quota_set'][update] = updates[update] self._update('/os-quota-sets/%s' % (tenant_id), body) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index eeb30ba..f1b06de 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -539,12 +539,19 @@ def do_credentials(cs, args): utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['token'], "Token") + _quota_resources = ['volumes', 'snapshots', 'gigabytes'] def _quota_show(quotas): quota_dict = {} - for resource in _quota_resources: + for resource in quotas._info.keys(): + good_name = False + for name in _quota_resources: + if resource.startswith(name): + good_name = True + if not good_name: + continue quota_dict[resource] = getattr(quotas, resource, None) utils.print_dict(quota_dict) @@ -554,6 +561,8 @@ def _quota_update(manager, identifier, args): for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: + if args.volume_type: + resource = resource + '_%s' % args.volume_type updates[resource] = val if updates: @@ -592,6 +601,10 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type (Optional, Default=None)') @utils.service_type('volume') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -622,6 +635,10 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type (Optional, Default=None)') @utils.service_type('volume') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index a4e2043..2d46a6d 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -34,20 +34,10 @@ class QuotaClassSetManager(base.Manager): return self._get("/os-quota-class-sets/%s" % (class_name), "quota_class_set") - def update(self, - class_name, - volumes=None, - snapshots=None, - gigabytes=None): + def update(self, class_name, **updates): + body = {'quota_class_set': {'class_name': class_name}} - body = {'quota_class_set': { - 'class_name': class_name, - 'volumes': volumes, - 'snapshots': snapshots, - 'gigabytes': gigabytes}} - - for key in list(body['quota_class_set'].keys()): - if body['quota_class_set'][key] is None: - body['quota_class_set'].pop(key) + for update in updates.keys(): + body['quota_class_set'][update] = updates[update] self._update('/os-quota-class-sets/%s' % (class_name), body) diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 30c4186..5b19b07 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -35,17 +35,11 @@ class QuotaSetManager(base.Manager): tenant_id = tenant_id.tenant_id return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") - def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None): + def update(self, tenant_id, **updates): + body = {'quota_set': {'tenant_id': tenant_id}} - body = {'quota_set': { - 'tenant_id': tenant_id, - 'volumes': volumes, - 'snapshots': snapshots, - 'gigabytes': gigabytes}} - - for key in list(body['quota_set'].keys()): - if body['quota_set'][key] is None: - body['quota_set'].pop(key) + for update in updates.keys(): + body['quota_set'][update] = updates[update] self._update('/os-quota-sets/%s' % (tenant_id), body) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 98883d6..fbba8ae 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -604,12 +604,19 @@ def do_credentials(cs, args): utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['token'], "Token") + _quota_resources = ['volumes', 'snapshots', 'gigabytes'] def _quota_show(quotas): quota_dict = {} - for resource in _quota_resources: + for resource in quotas._info.keys(): + good_name = False + for name in _quota_resources: + if resource.startswith(name): + good_name = True + if not good_name: + continue quota_dict[resource] = getattr(quotas, resource, None) utils.print_dict(quota_dict) @@ -619,6 +626,8 @@ def _quota_update(manager, identifier, args): for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: + if args.volume_type: + resource = resource + '_%s' % args.volume_type updates[resource] = val if updates: @@ -660,6 +669,10 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type (Optional, Default=None)') @utils.service_type('volume') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -692,6 +705,10 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type (Optional, Default=None)') @utils.service_type('volume') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" From 3d30126e93b66488b5c680578f6078201cdedc15 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Mon, 15 Jul 2013 16:25:40 +0000 Subject: [PATCH 43/51] Revert "Use exceptions from oslo" This reverts commit a7cce08eab5e2e42275b84bd56127bd09b00f5bf Change-Id: I6c0047adbc33d0d6b5890f11853974578c36c78c --- cinderclient/base.py | 2 +- cinderclient/client.py | 35 +- cinderclient/exceptions.py | 156 +++++- .../openstack/common/apiclient/__init__.py | 16 - .../openstack/common/apiclient/exceptions.py | 446 ------------------ cinderclient/shell.py | 2 +- cinderclient/tests/test_base.py | 2 +- cinderclient/tests/test_http.py | 2 +- cinderclient/tests/test_service_catalog.py | 2 +- cinderclient/tests/test_shell.py | 2 +- cinderclient/tests/test_utils.py | 2 +- cinderclient/tests/utils.py | 3 +- cinderclient/tests/v1/test_auth.py | 2 +- cinderclient/tests/v2/test_auth.py | 2 +- cinderclient/utils.py | 2 +- cinderclient/v1/shell.py | 2 +- cinderclient/v2/shell.py | 2 +- 17 files changed, 182 insertions(+), 498 deletions(-) delete mode 100644 cinderclient/openstack/common/apiclient/__init__.py delete mode 100644 cinderclient/openstack/common/apiclient/exceptions.py diff --git a/cinderclient/base.py b/cinderclient/base.py index ccbdd98..4e29078 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -25,7 +25,7 @@ import os import six -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import utils diff --git a/cinderclient/client.py b/cinderclient/client.py index 9a40dfc..857f80a 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -46,7 +46,7 @@ if not hasattr(urlparse, 'parse_qsl'): import requests -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import service_catalog from cinderclient import utils @@ -151,7 +151,7 @@ class HTTPClient(object): body = None if resp.status_code >= 400: - raise exceptions.from_response(resp, method, url) + raise exceptions.from_response(resp, body) return resp, body @@ -185,7 +185,7 @@ class HTTPClient(object): except exceptions.ClientException as e: if attempts > self.retries: raise - if 500 <= e.http_status <= 599: + if 500 <= e.code <= 599: pass else: raise @@ -211,16 +211,15 @@ class HTTPClient(object): def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) - def _extract_service_catalog(self, auth_url, url, method, - extract_token=True, **kwargs): + def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints. """ - resp, body = self.request(url, method, **kwargs) + if resp.status_code == 200: # content must always present try: - self.auth_url = auth_url + self.auth_url = url self.service_catalog = \ service_catalog.ServiceCatalog(body) @@ -249,7 +248,7 @@ class HTTPClient(object): elif resp.status_code == 305: return resp['location'] else: - raise exceptions.from_response(resp, method, url) + raise exceptions.from_response(resp, body) def _fetch_endpoints_from_auth(self, url): """We have a token, but don't know the final endpoint for @@ -264,14 +263,13 @@ class HTTPClient(object): """ # GET ...:5001/v2.0/tokens/#####/endpoints - auth_url = url url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) self._logger.debug("Using Endpoint URL: %s" % url) - return self._extract_service_catalog( - auth_url, - url, "GET", headers={'X-Auth-Token': self.auth_token}, - extract_token=False) + resp, body = self.request(url, "GET", + headers={'X-Auth-Token': self.auth_token}) + return self._extract_service_catalog(url, resp, body, + extract_token=False) def authenticate(self): magic_tuple = urlparse.urlsplit(self.auth_url) @@ -322,9 +320,7 @@ class HTTPClient(object): def _v1_auth(self, url): if self.proxy_token: - raise exceptions.AuthorizationFailure( - "This form of authentication does not support looking up" - " endpoints from an existing token.") + raise exceptions.NoTokenLookupException() headers = {'X-Auth-User': self.user, 'X-Auth-Key': self.password} @@ -343,7 +339,7 @@ class HTTPClient(object): elif resp.status_code == 305: return resp.headers['location'] else: - raise exceptions.from_response(resp, "GET", url) + raise exceptions.from_response(resp, body) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" @@ -373,13 +369,14 @@ class HTTPClient(object): token_url = url + "/tokens" # Make sure we follow redirects when trying to reach Keystone - return self._extract_service_catalog( - url, + resp, body = self.request( token_url, "POST", body=body, allow_redirects=True) + return self._extract_service_catalog(url, resp, body) + def get_volume_api_version_from_endpoint(self): magic_tuple = urlparse.urlsplit(self.management_url) scheme, netloc, path, query, frag = magic_tuple diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index e5e8678..0c52c9c 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -1,6 +1,156 @@ +# Copyright 2010 Jacob Kaplan-Moss """ -Backwards compatible exceptions module. +Exception definitions. """ -# flake8: noqa -from cinderclient.openstack.common.apiclient.exceptions import * + +class UnsupportedVersion(Exception): + """Indicates that the user is trying to use an unsupported + version of the API. + """ + pass + + +class InvalidAPIVersion(Exception): + pass + + +class CommandError(Exception): + pass + + +class AuthorizationFailure(Exception): + pass + + +class NoUniqueMatch(Exception): + pass + + +class NoTokenLookupException(Exception): + """This form of authentication does not support looking up + endpoints from an existing token. + """ + pass + + +class EndpointNotFound(Exception): + """Could not find Service or Region in Service Catalog.""" + pass + + +class AmbiguousEndpoints(Exception): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + self.endpoints = endpoints + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.endpoints) + + +class ClientException(Exception): + """ + The base exception class for all exceptions this library raises. + """ + def __init__(self, code, message=None, details=None, request_id=None): + self.code = code + self.message = message or self.__class__.message + self.details = details + self.request_id = request_id + + def __str__(self): + formatted_string = "%s (HTTP %s)" % (self.message, self.code) + if self.request_id: + formatted_string += " (Request-ID: %s)" % self.request_id + + return formatted_string + + +class BadRequest(ClientException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class OverLimit(ClientException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(ClientException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in ClientException.__subclasses__()) +# +# Instead, we have to hardcode it: +_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, + Forbidden, NotFound, + OverLimit, HTTPNotImplemented]) + + +def from_response(response, body): + """ + Return an instance of an ClientException or subclass + based on an requests response. + + Usage:: + + resp, body = requests.request(...) + if resp.status_code != 200: + raise exception_from_response(resp, rest.text) + """ + cls = _code_map.get(response.status_code, ClientException) + if response.headers: + request_id = response.headers.get('x-compute-request-id') + else: + request_id = None + if body: + message = "n/a" + details = "n/a" + if hasattr(body, 'keys'): + error = body[list(body.keys())[0]] + message = error.get('message', None) + details = error.get('details', None) + return cls(code=response.status_code, message=message, details=details, + request_id=request_id) + else: + return cls(code=response.status_code, request_id=request_id) diff --git a/cinderclient/openstack/common/apiclient/__init__.py b/cinderclient/openstack/common/apiclient/__init__.py deleted file mode 100644 index d5d0022..0000000 --- a/cinderclient/openstack/common/apiclient/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 OpenStack Foundation -# 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. diff --git a/cinderclient/openstack/common/apiclient/exceptions.py b/cinderclient/openstack/common/apiclient/exceptions.py deleted file mode 100644 index e70d37a..0000000 --- a/cinderclient/openstack/common/apiclient/exceptions.py +++ /dev/null @@ -1,446 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# 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. - -""" -Exception definitions. -""" - -import sys - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises. - """ - pass - - -class MissingArgs(ClientException): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = "Missing argument(s): %s" % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified a AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) - self.auth_system = auth_system - - -class NoUniqueMatch(ClientException): - """Multiple entities found instead of one.""" - pass - - -class EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -class AmbiguousEndpoints(EndpointException): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - super(AmbiguousEndpoints, self).__init__( - "AmbiguousEndpoints: %s" % repr(endpoints)) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions. - """ - http_status = 0 - message = "HTTP Error" - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HttpClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = "HTTP Client Error" - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = "HTTP Server Error" - - -class BadRequest(HttpClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = "Bad Request" - - -class Unauthorized(HttpClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = "Unauthorized" - - -class PaymentRequired(HttpClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = "Payment Required" - - -class Forbidden(HttpClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(HttpClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = "Not Found" - - -class MethodNotAllowed(HttpClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = "Method Not Allowed" - - -class NotAcceptable(HttpClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = "Not Acceptable" - - -class ProxyAuthenticationRequired(HttpClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = "Proxy Authentication Required" - - -class RequestTimeout(HttpClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = "Request Timeout" - - -class Conflict(HttpClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = "Conflict" - - -class Gone(HttpClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = "Gone" - - -class LengthRequired(HttpClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = "Length Required" - - -class PreconditionFailed(HttpClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = "Precondition Failed" - - -class RequestEntityTooLarge(HttpClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = "Request Entity Too Large" - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HttpClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = "Request-URI Too Long" - - -class UnsupportedMediaType(HttpClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = "Unsupported Media Type" - - -class RequestedRangeNotSatisfiable(HttpClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = "Requested Range Not Satisfiable" - - -class ExpectationFailed(HttpClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = "Expectation Failed" - - -class UnprocessableEntity(HttpClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = "Unprocessable Entity" - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = "Internal Server Error" - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = "Not Implemented" - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = "Bad Gateway" - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = "Service Unavailable" - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = "Gateway Timeout" - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = "HTTP Version Not Supported" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in HttpError.__subclasses__()) -_code_map = {} -for obj in sys.modules[__name__].__dict__.values(): - if isinstance(obj, type): - try: - http_status = obj.http_status - except AttributeError: - pass - else: - if http_status: - _code_map[http_status] = obj - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": response.headers.get("x-compute-request-id"), - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if hasattr(body, "keys"): - error = body[body.keys()[0]] - kwargs["message"] = error.get("message", None) - kwargs["details"] = error.get("details", None) - elif content_type.startswith("text/"): - kwargs["details"] = response.text - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HttpClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index c5b1dd2..d067996 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -32,8 +32,8 @@ import logging import six from cinderclient import client +from cinderclient import exceptions as exc import cinderclient.extension -from cinderclient.openstack.common.apiclient import exceptions as exc from cinderclient.openstack.common import strutils from cinderclient import utils from cinderclient.v1 import shell as shell_v1 diff --git a/cinderclient/tests/test_base.py b/cinderclient/tests/test_base.py index d22e2cd..75c37e6 100644 --- a/cinderclient/tests/test_base.py +++ b/cinderclient/tests/test_base.py @@ -1,5 +1,5 @@ from cinderclient import base -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient.v1 import volumes from cinderclient.tests import utils from cinderclient.tests.v1 import fakes diff --git a/cinderclient/tests/test_http.py b/cinderclient/tests/test_http.py index 8c8c99d..3b93a4b 100644 --- a/cinderclient/tests/test_http.py +++ b/cinderclient/tests/test_http.py @@ -3,7 +3,7 @@ import mock import requests from cinderclient import client -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient.tests import utils diff --git a/cinderclient/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py index 6db8966..c9d9819 100644 --- a/cinderclient/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -1,4 +1,4 @@ -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import service_catalog from cinderclient.tests import utils diff --git a/cinderclient/tests/test_shell.py b/cinderclient/tests/test_shell.py index b301c21..d6ef425 100644 --- a/cinderclient/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -5,7 +5,7 @@ import fixtures from six import moves from testtools import matchers -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions import cinderclient.shell from cinderclient.tests import utils diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index aec1587..8df482d 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -3,7 +3,7 @@ import sys from six import moves -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import utils from cinderclient import base from cinderclient.tests import utils as test_utils diff --git a/cinderclient/tests/utils.py b/cinderclient/tests/utils.py index b258d80..0ab8737 100644 --- a/cinderclient/tests/utils.py +++ b/cinderclient/tests/utils.py @@ -29,11 +29,10 @@ class TestResponse(requests.Response): def __init__(self, data): self._text = None - self.headers = {} super(TestResponse, self) if isinstance(data, dict): self.status_code = data.get('status_code', None) - self.headers = data.get('headers') or {} + self.headers = data.get('headers', None) # Fake the text attribute to streamline Response creation self._text = data.get('text', None) else: diff --git a/cinderclient/tests/v1/test_auth.py b/cinderclient/tests/v1/test_auth.py index f87e6db..704eacc 100644 --- a/cinderclient/tests/v1/test_auth.py +++ b/cinderclient/tests/v1/test_auth.py @@ -4,7 +4,7 @@ import mock import requests from cinderclient.v1 import client -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient.tests import utils diff --git a/cinderclient/tests/v2/test_auth.py b/cinderclient/tests/v2/test_auth.py index d4d0b19..89dd18f 100644 --- a/cinderclient/tests/v2/test_auth.py +++ b/cinderclient/tests/v2/test_auth.py @@ -19,7 +19,7 @@ import json import mock import requests -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient.v2 import client from cinderclient.tests import utils diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 90dadd3..922f053 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -23,7 +23,7 @@ import uuid import six import prettytable -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient.openstack.common import strutils diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index eeb30ba..a807854 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -22,7 +22,7 @@ import os import sys import time -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import utils diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 98883d6..5e85751 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -22,7 +22,7 @@ import time import six -from cinderclient.openstack.common.apiclient import exceptions +from cinderclient import exceptions from cinderclient import utils From 474c9ee58b7874a198a679c0e76328e659718e34 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 22 Jul 2013 15:20:04 -0500 Subject: [PATCH 44/51] Add availability-zone-list command This is client-side support for the os-availability-zone extension added in https://review.openstack.org/34813. As /os-availability-zone/detail is not yet implemented, adjustments were made to accomodate the lack of hosts in the returned zone list. Added for both v1 and v2, basically a copy of the equivalent novaclient command. Change-Id: Iae806a2b5ea3a2d3c984a138d9c27e169160766e --- cinderclient/tests/v1/fakes.py | 55 ++++++++++++ .../tests/v1/test_availability_zone.py | 87 +++++++++++++++++++ cinderclient/tests/v1/test_shell.py | 4 + cinderclient/tests/v2/fakes.py | 55 ++++++++++++ .../tests/v2/test_availability_zone.py | 87 +++++++++++++++++++ cinderclient/tests/v2/test_shell.py | 4 + cinderclient/v1/availability_zones.py | 42 +++++++++ cinderclient/v1/client.py | 3 + cinderclient/v1/shell.py | 67 ++++++++++++++ cinderclient/v2/availability_zones.py | 42 +++++++++ cinderclient/v2/client.py | 3 + cinderclient/v2/shell.py | 67 ++++++++++++++ 12 files changed, 516 insertions(+) create mode 100644 cinderclient/tests/v1/test_availability_zone.py create mode 100644 cinderclient/tests/v2/test_availability_zone.py create mode 100644 cinderclient/v1/availability_zones.py create mode 100644 cinderclient/v2/availability_zones.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 73ad1a9..c83f5ad 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -558,3 +558,58 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) + + def get_os_availability_zone(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None, + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-volume": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0) + } + } + } + }, + { + "zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0) + } + } + } + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) diff --git a/cinderclient/tests/v1/test_availability_zone.py b/cinderclient/tests/v1/test_availability_zone.py new file mode 100644 index 0000000..a2e1fc8 --- /dev/null +++ b/cinderclient/tests/v1/test_availability_zone.py @@ -0,0 +1,87 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. + +import six + +from cinderclient.v1 import availability_zones +from cinderclient.v1 import shell +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class AvailabilityZoneTest(utils.TestCase): + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = cs.availability_zones.list(detailed=False) + cs.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(2, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((len(z0), len(z1)), (1, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = cs.availability_zones.list(detailed=True) + cs.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(3, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- cinder-volume'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- cinder-sched'), + six.u('enabled :-) 2012-12-26 14:45:24')] + l6 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + z2 = shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z2[0], l6[0], l6[1]) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index a8c9425..71d7896 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -105,6 +105,10 @@ class ShellTest(utils.TestCase): self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + def test_list_availability_zone(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone') + def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index a140082..8f70e09 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -565,3 +565,58 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) + + def get_os_availability_zone(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None, + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-volume": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0) + } + } + } + }, + { + "zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0) + } + } + } + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) diff --git a/cinderclient/tests/v2/test_availability_zone.py b/cinderclient/tests/v2/test_availability_zone.py new file mode 100644 index 0000000..a2e1fc8 --- /dev/null +++ b/cinderclient/tests/v2/test_availability_zone.py @@ -0,0 +1,87 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. + +import six + +from cinderclient.v1 import availability_zones +from cinderclient.v1 import shell +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class AvailabilityZoneTest(utils.TestCase): + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = cs.availability_zones.list(detailed=False) + cs.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(2, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((len(z0), len(z1)), (1, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = cs.availability_zones.list(detailed=True) + cs.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(3, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- cinder-volume'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- cinder-sched'), + six.u('enabled :-) 2012-12-26 14:45:24')] + l6 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + z2 = shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z2[0], l6[0], l6[1]) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index eabf134..2405192 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -83,6 +83,10 @@ class ShellTest(utils.TestCase): self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + def test_list_availability_zone(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone') + def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') diff --git a/cinderclient/v1/availability_zones.py b/cinderclient/v1/availability_zones.py new file mode 100644 index 0000000..85ea9d7 --- /dev/null +++ b/cinderclient/v1/availability_zones.py @@ -0,0 +1,42 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. + +"""Availability Zone interface (v1 extension)""" + +from cinderclient import base + + +class AvailabilityZone(base.Resource): + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + + def list(self, detailed=False): + """Get a list of all availability zones + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 7dd84e1..1272c4e 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -14,6 +14,7 @@ # under the License. from cinderclient import client +from cinderclient.v1 import availability_zones from cinderclient.v1 import limits from cinderclient.v1 import quota_classes from cinderclient.v1 import quotas @@ -64,6 +65,8 @@ class Client(object): self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index ec62ba2..8144222 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -18,12 +18,14 @@ from __future__ import print_function import argparse +import copy import os import sys import time from cinderclient import exceptions from cinderclient import utils +from cinderclient.v1 import availability_zones def _poll_for_status(poll_fn, obj_id, action, final_ok_states, @@ -104,6 +106,11 @@ def _translate_volume_snapshot_keys(collection): _translate_keys(collection, convert) +def _translate_availability_zone_keys(collection): + convert = [('zoneName', 'name'), ('zoneState', 'status')] + _translate_keys(collection, convert) + + def _extract_metadata(args): metadata = {} for metadatum in args.metadata: @@ -859,3 +866,63 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" cs.services.disable(args.host, args.binary) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if getattr(zone, "hosts", None) and zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('volume') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status']) diff --git a/cinderclient/v2/availability_zones.py b/cinderclient/v2/availability_zones.py new file mode 100644 index 0000000..c8aef24 --- /dev/null +++ b/cinderclient/v2/availability_zones.py @@ -0,0 +1,42 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. + +"""Availability Zone interface (v2 extension)""" + +from cinderclient import base + + +class AvailabilityZone(base.Resource): + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + + def list(self, detailed=False): + """Get a list of all availability zones + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 003ecae..fea400f 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -14,6 +14,7 @@ # under the License. from cinderclient import client +from cinderclient.v1 import availability_zones from cinderclient.v2 import limits from cinderclient.v2 import quota_classes from cinderclient.v2 import quotas @@ -62,6 +63,8 @@ class Client(object): self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 5dc85d3..b49fded 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -16,6 +16,7 @@ from __future__ import print_function import argparse +import copy import os import sys import time @@ -24,6 +25,7 @@ import six from cinderclient import exceptions from cinderclient import utils +from cinderclient.v2 import availability_zones def _poll_for_status(poll_fn, obj_id, action, final_ok_states, @@ -98,6 +100,11 @@ def _translate_volume_snapshot_keys(collection): _translate_keys(collection, convert) +def _translate_availability_zone_keys(collection): + convert = [('zoneName', 'name'), ('zoneState', 'status')] + _translate_keys(collection, convert) + + def _extract_metadata(args): metadata = {} for metadatum in args.metadata[0]: @@ -944,3 +951,63 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" cs.services.disable(args.host, args.binary) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if getattr(zone, "hosts", None) and zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('volume') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status']) From 4a741587b4cafe2666e230c7d9420cfddea9df77 Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Thu, 25 Jul 2013 12:48:59 -0700 Subject: [PATCH 45/51] Updating HACKING file Using OpenStack HACKING file for common stuff and mentioning locals() disallowed now like Cinder. Change-Id: I05b1706eb52c13b9eb89fe5cbcce005c3cc75caf --- HACKING | 115 ---------------------------------------------------- HACKING.rst | 70 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 115 deletions(-) delete mode 100644 HACKING create mode 100644 HACKING.rst diff --git a/HACKING b/HACKING deleted file mode 100644 index 3b82d9c..0000000 --- a/HACKING +++ /dev/null @@ -1,115 +0,0 @@ -Cinder Style Commandments -========================= - -Step 1: Read http://www.python.org/dev/peps/pep-0008/ -Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -Step 3: Read on - -Imports -------- -- thou shalt not import objects, only modules -- thou shalt not import more than one module per line -- thou shalt not make relative imports -- thou shalt organize your imports according to the following template - -:: - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{cinder imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -General -------- -- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) -- thou shalt put one newline twixt methods in classes and anywhere else -- thou shalt not write "except:", use "except Exception:" at the very least -- thou shalt include your name with TODOs as in "TODO(termie)" -- thou shalt not name anything the same name as a builtin or reserved word -- thou shalt not violate causality in our time cone, or else - - -Human Alphabetical Order Examples ---------------------------------- -:: - import httplib - import logging - import random - import StringIO - import time - import unittest - - from cinder import flags - from cinder import test - from cinder.auth import users - from cinder.endpoint import api - from cinder.endpoint import cloud - -Docstrings ----------- - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. After - you have finished your descriptions add an extra newline and close the - quotations. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: description of the return value - - """ - -Text encoding ----------- -- All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - -- Transitions between internal unicode and external strings should always - be immediately and explicitly encoded or decoded. - -- All external text that is not explicitly encoded (database storage, - commandline arguments, etc.) should be presumed to be encoded as utf-8. - - WRONG: - - mystring = infile.readline() - myreturnstring = do_some_magic_with(mystring) - outfile.write(myreturnstring) - - RIGHT: - - mystring = infile.readline() - mytext = s.decode('utf-8') - returntext = do_some_magic_with(mytext) - returnstring = returntext.encode('utf-8') - outfile.write(returnstring) diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..ed887f5 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,70 @@ +Cinder Client Style Commandments +========================= + +- Step 1: Read the OpenStack Style Commandments + https://github.com/openstack-dev/hacking/blob/master/HACKING.rst +- Step 2: Read on + +Cinder Client Specific Commandments +---------------------------- + +General +------- +- Do not use locals(). Example:: + + LOG.debug(_("volume %(vol_name)s: creating size %(vol_size)sG") % + locals()) # BAD + + LOG.debug(_("volume %(vol_name)s: creating size %(vol_size)sG") % + {'vol_name': vol_name, + 'vol_size': vol_size}) # OKAY + +- Use 'raise' instead of 'raise e' to preserve original traceback or exception being reraised:: + + except Exception as e: + ... + raise e # BAD + + except Exception: + ... + raise # OKAY + +Text encoding +---------- +- All text within python code should be of type 'unicode'. + + WRONG: + + >>> s = 'foo' + >>> s + 'foo' + >>> type(s) + + + RIGHT: + + >>> u = u'foo' + >>> u + u'foo' + >>> type(u) + + +- Transitions between internal unicode and external strings should always + be immediately and explicitly encoded or decoded. + +- All external text that is not explicitly encoded (database storage, + commandline arguments, etc.) should be presumed to be encoded as utf-8. + + WRONG: + + mystring = infile.readline() + myreturnstring = do_some_magic_with(mystring) + outfile.write(myreturnstring) + + RIGHT: + + mystring = infile.readline() + mytext = s.decode('utf-8') + returntext = do_some_magic_with(mytext) + returnstring = returntext.encode('utf-8') + outfile.write(returnstring) From 854740c4c793444c955180e7c0c96977e3e06c56 Mon Sep 17 00:00:00 2001 From: Seif Lotfy Date: Thu, 18 Jul 2013 18:01:41 +0000 Subject: [PATCH 46/51] Add evaluation of --force parameter when creating snapshots Raise BadRequest Exception if value of --force parameter in snapshot-create is invalid Fixes: bug #1014689 Change-Id: If4858dc17cedf027112defb935016137727681cc --- cinderclient/v1/shell.py | 6 ++++++ cinderclient/v2/shell.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 856facb..4d41d6d 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -413,6 +413,12 @@ def do_snapshot_show(cs, args): @utils.service_type('volume') def do_snapshot_create(cs, args): """Add a new snapshot.""" + + if not args.force.lower() in ['true', '1', 'yes', 'y', + 'false', '0', 'no', 'n']: + msg = "Parameter 'force' does not support value '%s'" % args.force + raise exceptions.BadRequest(msg) + snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.display_name, diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 7bf15df..56921d5 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -470,6 +470,11 @@ def do_snapshot_create(cs, args): if args.display_description is not None: args.description = args.display_description + if not args.force.lower() in ['true', '1', 'yes', 'y', + 'false', '0', 'no', 'n']: + msg = "Parameter 'force' does not support value '%s'" % args.force + raise exceptions.BadRequest(msg) + snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.name, From 6193f2ba96632739913d4226c7ed8160f7010a82 Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Wed, 31 Jul 2013 16:03:17 -0700 Subject: [PATCH 47/51] Remove locals() from cinder client code base Hacking file now disallows locals() usage. Change-Id: I5049c718c2706d606c12913ae0b33a0fb3263542 --- cinderclient/utils.py | 8 ++++++-- cinderclient/v1/shell.py | 2 +- cinderclient/v2/shell.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 922f053..93be477 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -88,8 +88,12 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) if conflicting_keys and not allow_conflicts: - raise Exception("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'" % locals()) + msg = ("Hook '%(hook_name)s' is attempting to redefine attributes " + "'%(conflicting_keys)s'" % { + 'hook_name': hook_name, + 'conflicting_keys': conflicting_keys + }) + raise Exception(msg) extra_kwargs.update(hook_kwargs) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 82cb9f0..a2211fd 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -53,7 +53,7 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, print("\nFinished") break elif status == "error": - print("\nError %(action)s instance" % locals()) + print("\nError %(action)s instance" % {'action': action}) break else: print_progress(progress) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index b023125..fe2099c 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -51,7 +51,7 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, print("\nFinished") break elif status == "error": - print("\nError %(action)s instance" % locals()) + print("\nError %(action)s instance" % {'action': action}) break else: print_progress(progress) From aeab4e4335a94db6ca68950110bd9dd0f9d94b4f Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sat, 3 Aug 2013 00:09:23 +0000 Subject: [PATCH 48/51] Revert "Add evaluation of --force parameter when creating snapshots" This reverts commit 854740c4c793444c955180e7c0c96977e3e06c56 What we should've caught here is that the args actually passes in a Bool, which then fails the .lower(). Also, if we want to do things like check for strings representing bools we should use the utils.bool_from_str() method rather than write it inline like this. --- cinderclient/v1/shell.py | 6 ------ cinderclient/v2/shell.py | 5 ----- 2 files changed, 11 deletions(-) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 4d41d6d..856facb 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -413,12 +413,6 @@ def do_snapshot_show(cs, args): @utils.service_type('volume') def do_snapshot_create(cs, args): """Add a new snapshot.""" - - if not args.force.lower() in ['true', '1', 'yes', 'y', - 'false', '0', 'no', 'n']: - msg = "Parameter 'force' does not support value '%s'" % args.force - raise exceptions.BadRequest(msg) - snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.display_name, diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 56921d5..7bf15df 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -470,11 +470,6 @@ def do_snapshot_create(cs, args): if args.display_description is not None: args.description = args.display_description - if not args.force.lower() in ['true', '1', 'yes', 'y', - 'false', '0', 'no', 'n']: - msg = "Parameter 'force' does not support value '%s'" % args.force - raise exceptions.BadRequest(msg) - snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.name, From e04232b552634a7429c94120cd02c12d86a192c1 Mon Sep 17 00:00:00 2001 From: Jakub Ruzicka Date: Wed, 31 Jul 2013 13:07:14 -0400 Subject: [PATCH 49/51] Provide cinder CLI man page. Provide basic but hopefully useful man page. shell.rst was merged into and replaced by the man page in HTML docs. pbr is used to determine version. Docs copyright was changed to more accurate "OpenStack Contributors". Fixes: bug 1206968 Implements: blueprint clients-man-pages Change-Id: Iedd7b4b161ced564833fd9433762b87a4c1a374d --- doc/source/conf.py | 13 ++++++--- doc/source/index.rst | 2 ++ doc/source/man/cinder.rst | 58 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 doc/source/man/cinder.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index a89528d..d4ae7ca 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,6 +13,7 @@ import os import sys +import pbr.version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -43,16 +44,16 @@ master_doc = 'index' # General information about the project. project = 'python-cinderclient' -copyright = 'Rackspace, based on work by Jacob Kaplan-Moss' +copyright = 'OpenStack Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# +version_info = pbr.version.VersionInfo('python-cinderclient') # The short X.Y version. -version = '2.6' +version = version_info.version_string() # The full version, including alpha/beta/rc tags. -release = '2.6.10' +release = version_info.release_string() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -93,6 +94,10 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] +man_pages = [ + ('man/cinder', 'cinder', u'Client for OpenStack Block Storage API', + [u'OpenStack Contributors'], 1), +] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with diff --git a/doc/source/index.rst b/doc/source/index.rst index 784a665..2760656 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -24,6 +24,8 @@ In order to use the CLI, you must provide your OpenStack username, password, ten Once you've configured your authentication parameters, you can run ``cinder help`` to see a complete listing of available commands. +See also :doc:`/man/cinder`. + Release Notes ============= diff --git a/doc/source/man/cinder.rst b/doc/source/man/cinder.rst new file mode 100644 index 0000000..50fb644 --- /dev/null +++ b/doc/source/man/cinder.rst @@ -0,0 +1,58 @@ +============================== +:program:`cinder` CLI man page +============================== + +.. program:: cinder +.. highlight:: bash + + +SYNOPSIS +======== + +:program:`cinder` [options] [command-options] + +:program:`cinder help` + +:program:`cinder help` + + +DESCRIPTION +=========== + +The :program:`cinder` command line utility interacts with OpenStack Block +Storage Service (Cinder). + +In order to use the CLI, you must provide your OpenStack username, password, +project (historically called tenant), and auth endpoint. You can use +configuration options :option:`--os-username`, :option:`--os-password`, +:option:`--os-tenant-name` or :option:`--os-tenant-id`, and +:option:`--os-auth-url` or set corresponding environment variables:: + + export OS_USERNAME=user + export OS_PASSWORD=pass + export OS_TENANT_NAME=myproject + export OS_AUTH_URL=http://auth.example.com:5000/v2.0 + +You can select an API version to use by :option:`--os-volume-api-version` +option or by setting corresponding environment variable:: + + export OS_VOLUME_API_VERSION=2 + + +OPTIONS +======= + +To get a list of available commands and options run:: + + cinder help + +To get usage and options of a command:: + + cinder help + + +BUGS +==== + +Cinder client is hosted in Launchpad so you can view current bugs at +https://bugs.launchpad.net/python-cinderclient/. From 61006510007f7b23a25bf0bfcd8ad7d740def9e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Aug 2013 13:36:45 -0300 Subject: [PATCH 50/51] Sync with global requirements Change-Id: Iccc824fef7dc7ae5675d6528a1ea33566e5f7eef --- cinderclient/exceptions.py | 14 ++++++++++++++ cinderclient/tests/fakes.py | 13 +++++++++++++ cinderclient/tests/test_base.py | 13 +++++++++++++ cinderclient/tests/test_client.py | 13 +++++++++++++ cinderclient/tests/test_http.py | 13 +++++++++++++ cinderclient/tests/test_service_catalog.py | 13 +++++++++++++ cinderclient/tests/test_shell.py | 13 +++++++++++++ cinderclient/tests/test_utils.py | 13 +++++++++++++ cinderclient/tests/utils.py | 13 +++++++++++++ .../tests/v1/contrib/test_list_extensions.py | 13 +++++++++++++ cinderclient/tests/v1/test_auth.py | 13 +++++++++++++ cinderclient/tests/v1/test_types.py | 13 +++++++++++++ cinderclient/tests/v1/test_volumes.py | 13 +++++++++++++ cinderclient/v1/limits.py | 15 ++++++++++++++- cinderclient/v2/limits.py | 15 ++++++++++++++- requirements.txt | 3 +-- setup.py | 5 +++-- test-requirements.txt | 4 ++-- 18 files changed, 204 insertions(+), 8 deletions(-) diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index 0c52c9c..55c0757 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -1,4 +1,18 @@ # Copyright 2010 Jacob Kaplan-Moss +# +# 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. + """ Exception definitions. """ diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index a6872bc..29260e1 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -1,3 +1,16 @@ +# 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. + """ A fake server that "responds" to API methods with pre-canned responses. diff --git a/cinderclient/tests/test_base.py b/cinderclient/tests/test_base.py index 75c37e6..508c03f 100644 --- a/cinderclient/tests/test_base.py +++ b/cinderclient/tests/test_base.py @@ -1,3 +1,16 @@ +# 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 import base from cinderclient import exceptions from cinderclient.v1 import volumes diff --git a/cinderclient/tests/test_client.py b/cinderclient/tests/test_client.py index 17b2b88..47c4c69 100644 --- a/cinderclient/tests/test_client.py +++ b/cinderclient/tests/test_client.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import cinderclient.client import cinderclient.v1.client diff --git a/cinderclient/tests/test_http.py b/cinderclient/tests/test_http.py index 3b93a4b..f38c6f1 100644 --- a/cinderclient/tests/test_http.py +++ b/cinderclient/tests/test_http.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import mock import requests diff --git a/cinderclient/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py index c9d9819..1025bd5 100644 --- a/cinderclient/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -1,3 +1,16 @@ +# 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 import exceptions from cinderclient import service_catalog from cinderclient.tests import utils diff --git a/cinderclient/tests/test_shell.py b/cinderclient/tests/test_shell.py index d6ef425..9692847 100644 --- a/cinderclient/tests/test_shell.py +++ b/cinderclient/tests/test_shell.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import re import sys diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index 8df482d..32db04c 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import collections import sys diff --git a/cinderclient/tests/utils.py b/cinderclient/tests/utils.py index 0ab8737..0deb579 100644 --- a/cinderclient/tests/utils.py +++ b/cinderclient/tests/utils.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os import fixtures diff --git a/cinderclient/tests/v1/contrib/test_list_extensions.py b/cinderclient/tests/v1/contrib/test_list_extensions.py index 8066c54..96f5e35 100644 --- a/cinderclient/tests/v1/contrib/test_list_extensions.py +++ b/cinderclient/tests/v1/contrib/test_list_extensions.py @@ -1,3 +1,16 @@ +# 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 import extension from cinderclient.v1.contrib import list_extensions diff --git a/cinderclient/tests/v1/test_auth.py b/cinderclient/tests/v1/test_auth.py index 704eacc..b681dc7 100644 --- a/cinderclient/tests/v1/test_auth.py +++ b/cinderclient/tests/v1/test_auth.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import json import mock diff --git a/cinderclient/tests/v1/test_types.py b/cinderclient/tests/v1/test_types.py index 6d7dd28..6212336 100644 --- a/cinderclient/tests/v1/test_types.py +++ b/cinderclient/tests/v1/test_types.py @@ -1,3 +1,16 @@ +# 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.v1 import volume_types from cinderclient.tests import utils from cinderclient.tests.v1 import fakes diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index 768e942..0da88e2 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -1,3 +1,16 @@ +# 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 import utils from cinderclient.tests.v1 import fakes diff --git a/cinderclient/v1/limits.py b/cinderclient/v1/limits.py index f76cd68..32421e8 100644 --- a/cinderclient/v1/limits.py +++ b/cinderclient/v1/limits.py @@ -1,4 +1,17 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. from cinderclient import base diff --git a/cinderclient/v2/limits.py b/cinderclient/v2/limits.py index 87349b6..16eb515 100644 --- a/cinderclient/v2/limits.py +++ b/cinderclient/v2/limits.py @@ -1,4 +1,17 @@ -# Copyright 2013 OpenStack LLC. +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. from cinderclient import base diff --git a/requirements.txt b/requirements.txt index ab80cff..068a2f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ -d2to1>=0.2.10,<0.3 pbr>=0.5.16,<0.6 argparse -prettytable>=0.6,<0.8 +PrettyTable>=0.6,<0.8 requests>=1.1,<1.2.3 simplejson>=2.0.9 six diff --git a/setup.py b/setup.py index 1e9882d..15f4e9d 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools setuptools.setup( - setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5,<0.6'], - d2to1=True) + setup_requires=['pbr>=0.5.20'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index ff2d884..30c33c0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ pep8==1.4.5 pyflakes==0.7.2 flake8==2.0 -hacking>=0.5.6,<0.6 +hacking>=0.5.6,<0.7 coverage>=3.6 discover fixtures>=0.3.12 @@ -10,4 +10,4 @@ mock>=0.8.0 python-subunit sphinx>=1.1.2 testtools>=0.9.32 -testrepository>=0.0.15 +testrepository>=0.0.17 From 8a107396c242702de9a7df819eb82e5f55051722 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Tue, 6 Aug 2013 16:00:51 -0600 Subject: [PATCH 51/51] Add a couple more things to index before release Most of the new features are in and oslo req's are synched, making the cut off here and getting a new client version pushed up to PyPi. Change-Id: If208b2341db06b438851d7b2aaab541efc79ea35 --- doc/source/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 2760656..3a658b1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -31,6 +31,8 @@ Release Notes ============= 1.0.5 ----- +* Add CLI man page +* Add Availability Zone list command * Add support for scheduler-hints * Add support to extend volumes * Add support to reset state on volumes and snapshots