From f80509ff1a1319ed6c7caa8a7dda6534fe42106d Mon Sep 17 00:00:00 2001 From: Liam Kelleher Date: Wed, 27 Mar 2013 15:06:11 +0000 Subject: [PATCH 001/165] Add OS_TENANT_ID as authentication option bug 1160898 Change-Id: I8849ab8f552801b8a317cd0394251c2db1ba8e9a --- cinderclient/shell.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index b163d70..0fe345e 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -114,6 +114,14 @@ class OpenStackCinderShell(object): parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) + parser.add_argument('--os-tenant-id', + metavar='', + default=utils.env('OS_TENANT_ID', + 'CINDER_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID].') + parser.add_argument('--os_tenant_id', + help=argparse.SUPPRESS) + parser.add_argument('--os-auth-url', metavar='', default=utils.env('OS_AUTH_URL', @@ -350,13 +358,14 @@ class OpenStackCinderShell(object): return 0 (os_username, os_password, os_tenant_name, os_auth_url, - os_region_name, endpoint_type, insecure, + os_region_name, os_tenant_id, endpoint_type, insecure, service_type, service_name, volume_service_name, username, apikey, projectid, url, region_name, cacert) = ( args.os_username, args.os_password, args.os_tenant_name, args.os_auth_url, - args.os_region_name, args.endpoint_type, - args.insecure, args.service_type, args.service_name, + args.os_region_name, args.os_tenant_id, + args.endpoint_type, args.insecure, + args.service_type, args.service_name, args.volume_service_name, args.username, args.apikey, args.projectid, args.url, args.region_name, args.os_cacert) @@ -388,11 +397,11 @@ class OpenStackCinderShell(object): else: os_password = apikey - if not os_tenant_name: + if not (os_tenant_name or os_tenant_id): if not projectid: - raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or " - "env[OS_TENANT_NAME]") + raise exc.CommandError("You must provide a tenant_id " + "via either --os-tenant-id or " + "env[OS_TENANT_ID]") else: os_tenant_name = projectid @@ -407,10 +416,10 @@ class OpenStackCinderShell(object): if not os_region_name and region_name: os_region_name = region_name - if not os_tenant_name: + if not (os_tenant_name or os_tenant_id): raise exc.CommandError( - "You must provide a tenant name " - "via either --os-tenant-name or env[OS_TENANT_NAME]") + "You must provide a tenant_id " + "via either --os-tenant-id or env[OS_TENANT_ID]") if not os_auth_url: raise exc.CommandError( @@ -420,6 +429,7 @@ class OpenStackCinderShell(object): self.cs = client.Client(options.os_volume_api_version, os_username, os_password, os_tenant_name, os_auth_url, insecure, region_name=os_region_name, + tenant_id=os_tenant_id, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, From c83e4a4ba9b8bb66fb2ace91e123d30f16bcc5c1 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Tue, 2 Apr 2013 17:12:22 -0600 Subject: [PATCH 002/165] Pin prettytable versions pip-requires doesn't have any restrictions on prettytable. We should match up with the pinned version in other projects. In addition to avoid this problem in the future updated simplejson to match openstack/requirements as well. Change-Id: Iabf7ee8b0fd253447d1a65bc1b12906d26176286 --- tools/pip-requires | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/pip-requires b/tools/pip-requires index e708630..88b00d2 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,4 +1,4 @@ argparse -prettytable +prettytable>=0.6,<0.8 requests>=0.8 -simplejson +simplejson>=2.0.9 From 952970acbd1bf926303d84881510c175b5395e98 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Thu, 4 Apr 2013 10:30:37 -0400 Subject: [PATCH 003/165] Fix X-Auth_Token -> X-Auth-Token header name Fixes bug 1163546 Change-Id: Idb2c5f23a1c57676984ffa33f64d27c461b0de15 --- cinderclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 2b58a04..92019f7 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -248,7 +248,7 @@ class HTTPClient(object): % (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}) + headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, extract_token=False) From 130963e93939759e800c45d79b2fdddc9cdbe234 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Thu, 4 Apr 2013 16:39:43 -0400 Subject: [PATCH 004/165] Fix result -> resp typo in unset_keys Change-Id: I07b0ea54993d5b00060e1dfcdfc10663d091254b --- cinderclient/v1/volume_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/v1/volume_types.py b/cinderclient/v1/volume_types.py index f230786..93eabd3 100644 --- a/cinderclient/v1/volume_types.py +++ b/cinderclient/v1/volume_types.py @@ -65,7 +65,7 @@ class VolumeType(base.Resource): # the return in the loop resulted in ony ONE key being unset. # since on success the return was NONE, we'll only interrupt the loop # and return if there's an error - result = None + resp = None for k in keys: resp = self.manager._delete( "/types/%s/extra_specs/%s" % ( From 5e4a985246c4c505be30447944bdd80cb51820fc Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Thu, 4 Apr 2013 16:46:55 -0400 Subject: [PATCH 005/165] Remove unused "import sys" Cleaning up with pyflakes. Change-Id: I24878f1bb1693af13b2a7f0f7d6fe455f8747e91 --- cinderclient/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 2b58a04..37c0512 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -9,7 +9,6 @@ OpenStack Client interface. Handles the REST calls and responses. import logging import os -import sys import urlparse try: from eventlet import sleep From fd3351ffa81d5c1aef336d789ebcf8c26b26d53c Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Tue, 2 Apr 2013 10:07:24 -0400 Subject: [PATCH 006/165] Minor typo/message fixes Correct it's -> its in force-delete message Print whole bash_completion message in "cinder help" Reformat some doc strings per PEP-0257 Change-Id: I013b849508beac5c9fe5c6f9d4cdfae54676c29c --- HACKING | 4 +-- cinderclient/shell.py | 3 ++- cinderclient/v1/shell.py | 2 +- cinderclient/v1/volumes.py | 51 +++++++++++--------------------------- cinderclient/v2/shell.py | 2 +- cinderclient/v2/volumes.py | 2 +- 6 files changed, 21 insertions(+), 43 deletions(-) diff --git a/HACKING b/HACKING index d9d1cb8..3b82d9c 100644 --- a/HACKING +++ b/HACKING @@ -1,5 +1,5 @@ -Nova Style Commandments -======================= +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 diff --git a/cinderclient/shell.py b/cinderclient/shell.py index c080564..2a52685 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -446,7 +446,8 @@ class OpenStackCinderShell(object): extension.run_hooks(hook_type, *args, **kwargs) def do_bash_completion(self, args): - """ + """Print arguments for bash_completion. + Prints all of the commands and options to stdout so that the cinder.bash_completion script doesn't have to hard code them. """ diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 8119654..a6f6252 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -256,7 +256,7 @@ def do_delete(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to delete.') @utils.service_type('volume') def do_force_delete(cs, args): - """Attempt forced removal of a volume, regardless of it's state.""" + """Attempt forced removal of a volume, regardless of its state.""" volume = _find_volume(cs, args.volume) volume.force_delete() diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 078cd6f..7ab7e3c 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -22,27 +22,20 @@ from cinderclient import base class Volume(base.Resource): - """ - A volume is an extra block level storage to the OpenStack instances. - """ + """A volume is an extra block level storage to the OpenStack instances.""" def __repr__(self): return "" % self.id def delete(self): - """ - Delete this volume. - """ + """Delete this volume.""" self.manager.delete(self) def update(self, **kwargs): - """ - Update the display_name or display_description for this volume. - """ + """Update the display_name or display_description for this volume.""" self.manager.update(self, **kwargs) def attach(self, instance_uuid, mountpoint): - """ - Set attachment metadata. + """Set attachment metadata. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance. @@ -50,54 +43,41 @@ class Volume(base.Resource): return self.manager.attach(self, instance_uuid, mountpoint) def detach(self): - """ - Clear attachment metadata. - """ + """Clear attachment metadata.""" return self.manager.detach(self) def reserve(self, volume): - """ - Reserve this volume. - """ + """Reserve this volume.""" return self.manager.reserve(self) def unreserve(self, volume): - """ - Unreserve this volume. - """ + """Unreserve this volume.""" return self.manager.unreserve(self) def begin_detaching(self, volume): - """ - Begin detaching volume. - """ + """Begin detaching volume.""" return self.manager.begin_detaching(self) def roll_detaching(self, volume): - """ - Roll detaching volume. - """ + """Roll detaching volume.""" return self.manager.roll_detaching(self) def initialize_connection(self, volume, connector): - """ - Initialize a volume connection. + """Initialize a volume connection. :param connector: connector dict from nova. """ return self.manager.initialize_connection(self, connector) def terminate_connection(self, volume, connector): - """ - Terminate a volume connection. + """Terminate a volume connection. :param connector: connector dict from nova. """ return self.manager.terminate_connection(self, connector) def set_metadata(self, volume, metadata): - """ - Set or Append metadata to a volume. + """Set or Append metadata to a volume. :param type : The :class: `Volume` to set metadata on :param metadata: A dict of key/value pairs to set @@ -106,15 +86,12 @@ 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. - """ + """Upload a volume to image service as an image.""" self.manager.upload_to_image(self, force, image_name, container_format, disk_format) def force_delete(self): - """ - Delete the specififed volume ignoring it's current state. + """Delete the specified volume ignoring its current state. :param volume: The UUID of the volume to force-delete. """ diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 2cf7c76..ee0ed23 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -266,7 +266,7 @@ def do_delete(cs, args): help='ID of the volume to delete.') @utils.service_type('volume') def do_force_delete(cs, args): - """Attempt forced removal of a volume, regardless of it's state.""" + """Attempt forced removal of a volume, regardless of its state.""" volume = _find_volume(cs, args.volume) volume.force_delete() diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 4e9b86b..cf9f9ac 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -90,7 +90,7 @@ class Volume(base.Resource): disk_format) def force_delete(self): - """Delete the specififed volume ignoring it's current state. + """Delete the specified volume ignoring its current state. :param volume: The UUID of the volume to force-delete. """ From dca8dbd1c840b53bbe1c36c78f0df4d2a6b16445 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Sun, 7 Apr 2013 20:42:35 -0400 Subject: [PATCH 007/165] Sync with oslo-incubator copy of setup.py and version.py Keep up with the changes to the master copy Change-Id: I24522c7de91c2dea0f7e4b8cac2a27ff162df7f1 --- cinderclient/openstack/common/setup.py | 72 +++++++++++++++++------- cinderclient/openstack/common/version.py | 16 ++++-- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/cinderclient/openstack/common/setup.py b/cinderclient/openstack/common/setup.py index fb187ff..ba6b54a 100644 --- a/cinderclient/openstack/common/setup.py +++ b/cinderclient/openstack/common/setup.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation. # Copyright 2012-2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # @@ -43,6 +43,11 @@ def parse_mailmap(mailmap='.mailmap'): 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. @@ -117,9 +122,9 @@ def _run_shell_command(cmd, throw_on_error=False): 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) - out = output.communicate() if len(out) == 0: return None if len(out[0].strip()) == 0: @@ -127,14 +132,26 @@ def _run_shell_command(cmd, throw_on_error=False): 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 os.path.isdir('.git'): - git_log_cmd = 'git log --stat' + if git_dir: + git_log_cmd = 'git --git-dir=%s log' % git_dir changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() + mailmap = _parse_git_mailmap(git_dir) with open(new_changelog, "w") as changelog_file: changelog_file.write(canonicalize_emails(changelog, mailmap)) else: @@ -146,13 +163,23 @@ def generate_authors(): 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 os.path.isdir('.git'): + if git_dir: # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " + 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) - mailmap = parse_mailmap() + 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): @@ -258,43 +285,48 @@ def get_cmdclass(): return cmdclass -def _get_revno(): +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 describe --always") + 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 rev-list --abbrev-commit HEAD") + 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): +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.""" - if os.path.isdir('.git'): + git_dir = _get_git_directory() + if git_dir: if pre_version: try: return _run_shell_command( - "git describe --exact-match", + "git --git-dir=" + git_dir + " describe --exact-match", throw_on_error=True).replace('-', '.') except Exception: - sha = _run_shell_command("git log -n1 --pretty=format:%h") - return "%s.a%s.g%s" % (pre_version, _get_revno(), sha) + 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 describe --always").replace('-', '.') + "git --git-dir=" + git_dir + " describe --always").replace( + '-', '.') return None -def get_version_from_pkg_info(package_name): +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') @@ -325,10 +357,10 @@ def get_version(package_name, pre_version=None): version = os.environ.get("OSLO_PACKAGE_VERSION", None) if version: return version - version = get_version_from_pkg_info(package_name) + version = _get_version_from_pkg_info(package_name) if version: return version - version = get_version_from_git(pre_version) + version = _get_version_from_git(pre_version) if version: return version raise Exception("Versioning for this project requires either an sdist" diff --git a/cinderclient/openstack/common/version.py b/cinderclient/openstack/common/version.py index a3559d8..c476d19 100644 --- a/cinderclient/openstack/common/version.py +++ b/cinderclient/openstack/common/version.py @@ -1,5 +1,5 @@ -# Copyright 2012 OpenStack LLC +# 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 @@ -33,6 +33,14 @@ class VersionInfo(object): 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.""" @@ -41,11 +49,11 @@ class VersionInfo(object): 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 with + # 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. Check for a PKG-INFO file. + # installed into anything. Revert to setup-time logic. from cinderclient.openstack.common import setup - return setup.get_version_from_pkg_info(self.package) + return setup.get_version(self.package) def release_string(self): """Return the full version of the package including suffixes indicating From 0b781536b26e57903b297d4d4e7806e4a051a532 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Thu, 11 Apr 2013 19:17:01 -0600 Subject: [PATCH 008/165] Don't print the empty table on list operations. Changes to strutils.safe_encode cause the empty table to be printed out when no objects are actually retrieved. This patch just adds a check of the object list length before calling strutils.safe_encode, if the length is NOT > 0 we just return like we used to. Change-Id: I57930b5210c975baa7c4510fcf82b17157e17788 --- cinderclient/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index f9c6566..558c292 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -143,7 +143,8 @@ def print_list(objs, fields, formatters={}): row.append(data) pt.add_row(row) - print strutils.safe_encode(pt.get_string(sortby=fields[0])) + if len(objs) > 0: + print strutils.safe_encode(pt.get_string(sortby=fields[0])) def print_dict(d, property="Property"): From c476311a28eb4d21a5f70bf8db90d67490f88154 Mon Sep 17 00:00:00 2001 From: Stephen Mulcahy Date: Fri, 12 Apr 2013 13:37:40 +0000 Subject: [PATCH 009/165] Fixed unit test name in v1 and v2 tests Change-Id: Ia6a88be09a07ed269b1e9b43a86c88731c80784c --- tests/v1/test_volumes.py | 2 +- tests/v2/test_volumes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/v1/test_volumes.py b/tests/v1/test_volumes.py index 913e5a9..3306fdb 100644 --- a/tests/v1/test_volumes.py +++ b/tests/v1/test_volumes.py @@ -16,7 +16,7 @@ class VolumesTest(utils.TestCase): cs.volumes.delete(v) cs.assert_called('DELETE', '/volumes/1234') - def test_create_keypair(self): + def test_create_volume(self): cs.volumes.create(1) cs.assert_called('POST', '/volumes') diff --git a/tests/v2/test_volumes.py b/tests/v2/test_volumes.py index 85401af..8e46da6 100644 --- a/tests/v2/test_volumes.py +++ b/tests/v2/test_volumes.py @@ -32,7 +32,7 @@ class VolumesTest(utils.TestCase): cs.volumes.delete(v) cs.assert_called('DELETE', '/volumes/1234') - def test_create_keypair(self): + def test_create_volume(self): cs.volumes.create(1) cs.assert_called('POST', '/volumes') From cc8dd55264b2778e082eb57be1cdeb5fee44fc4b Mon Sep 17 00:00:00 2001 From: Stephen Mulcahy Date: Fri, 12 Apr 2013 10:36:25 +0000 Subject: [PATCH 010/165] Add support for volume backups Implements support for the volume backup api added to cinder in https://review.openstack.org/19468. This is a resubmit of the expired change https://review.openstack.org/25299 with the following changes: * Added unit tests for backups (v1 and v2) * Changed references in backup code to display_name and display_description to name and description respectively * Removed links from backup-show output * Added object_count to _translate_backup_keys * Removed unneccesary items from _translate_backup_keys * Fixed backups docstrings including removing references to swift. Adds backup-create, backup-delete, backup-list, backup-restore and backup-show to both the v1 and v2 clients, since the volume backup extension is available via both APIs. Change-Id: I197384f1c2fd2af641d207a5f4dba0dfbc5c681a --- cinderclient/v1/client.py | 4 + cinderclient/v1/shell.py | 83 ++++++++++++++++++-- cinderclient/v1/volume_backups.py | 76 ++++++++++++++++++ cinderclient/v1/volume_backups_restore.py | 43 +++++++++++ cinderclient/v2/client.py | 4 + cinderclient/v2/shell.py | 94 +++++++++++++++++++++-- cinderclient/v2/volume_backups.py | 76 ++++++++++++++++++ cinderclient/v2/volume_backups_restore.py | 43 +++++++++++ tests/v1/fakes.py | 89 +++++++++++++++++++++ tests/v1/test_volume_backups.py | 53 +++++++++++++ tests/v2/fakes.py | 89 +++++++++++++++++++++ tests/v2/test_volume_backups.py | 53 +++++++++++++ 12 files changed, 693 insertions(+), 14 deletions(-) create mode 100644 cinderclient/v1/volume_backups.py create mode 100644 cinderclient/v1/volume_backups_restore.py create mode 100644 cinderclient/v2/volume_backups.py create mode 100644 cinderclient/v2/volume_backups_restore.py create mode 100644 tests/v1/test_volume_backups.py create mode 100644 tests/v2/test_volume_backups.py diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index efdf3e2..9d71b60 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -5,6 +5,8 @@ from cinderclient.v1 import quotas from cinderclient.v1 import volumes 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 class Client(object): @@ -41,6 +43,8 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) + self.backups = volume_backups.VolumeBackupManager(self) + self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index a6f6252..214a475 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -66,6 +66,11 @@ def _find_volume_snapshot(cs, snapshot): return utils.find_resource(cs.volume_snapshots, snapshot) +def _find_backup(cs, backup): + """Get a backup by ID.""" + return utils.find_resource(cs.backups, backup) + + def _print_volume(volume): utils.print_dict(volume._info) @@ -74,8 +79,7 @@ def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) -def _translate_volume_keys(collection): - convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')] +def _translate_keys(collection, convert): for item in collection: keys = item.__dict__.keys() for from_key, to_key in convert: @@ -83,13 +87,14 @@ def _translate_volume_keys(collection): setattr(item, to_key, item._info[from_key]) +def _translate_volume_keys(collection): + convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')] + _translate_keys(collection, convert) + + def _translate_volume_snapshot_keys(collection): convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')] - for item in collection: - keys = 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]) + _translate_keys(collection, convert) def _extract_metadata(args): @@ -648,3 +653,67 @@ def do_upload_to_image(cs, args): args.image_name, args.container_format, args.disk_format) + + +@utils.arg('volume', metavar='', + help='ID of the volume to backup.') +@utils.arg('--container', metavar='', + help='Optional Backup container name. (Default=None)', + default=None) +@utils.arg('--display-name', metavar='', + help='Optional backup name. (Default=None)', + default=None) +@utils.arg('--display-description', metavar='', + help='Optional backup description. (Default=None)', + default=None) +@utils.service_type('volume') +def do_backup_create(cs, args): + """Creates a backup.""" + cs.backups.create(args.volume, + args.container, + args.display_name, + args.display_description) + + +@utils.arg('backup', metavar='', help='ID of the backup.') +@utils.service_type('volume') +def do_backup_show(cs, args): + """Show details about a backup.""" + backup = _find_backup(cs, args.backup) + info = dict() + info.update(backup._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.service_type('volume') +def do_backup_list(cs, args): + """List all the backups.""" + backups = cs.backups.list() + columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', + 'Container'] + utils.print_list(backups, columns) + + +@utils.arg('backup', metavar='', + help='ID of the backup to delete.') +@utils.service_type('volume') +def do_backup_delete(cs, args): + """Remove a backup.""" + backup = _find_backup(cs, args.backup) + backup.delete() + + +@utils.arg('backup', metavar='', + help='ID of the backup to restore.') +@utils.arg('--volume-id', metavar='', + help='Optional ID of the volume to restore to.', + default=None) +@utils.service_type('volume') +def do_backup_restore(cs, args): + """Restore a backup.""" + cs.restores.restore(args.backup, + args.volume_id) diff --git a/cinderclient/v1/volume_backups.py b/cinderclient/v1/volume_backups.py new file mode 100644 index 0000000..89056af --- /dev/null +++ b/cinderclient/v1/volume_backups.py @@ -0,0 +1,76 @@ +# 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 Backups interface (1.1 extension). +""" + +from cinderclient import base + + +class VolumeBackup(base.Resource): + """A volume backup is a block level backup of a volume.""" + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume backup.""" + return self.manager.delete(self) + + +class VolumeBackupManager(base.ManagerWithFind): + """Manage :class:`VolumeBackup` resources.""" + resource_class = VolumeBackup + + def create(self, volume_id, container=None, + name=None, description=None): + """Create a volume backup. + + :param volume_id: The ID of the volume to backup. + :param container: The name of the backup service container. + :param name: The name of the backup. + :param description: The description of the backup. + :rtype: :class:`VolumeBackup` + """ + body = {'backup': {'volume_id': volume_id, + 'container': container, + 'name': name, + 'description': description}} + return self._create('/backups', body, 'backup') + + def get(self, backup_id): + """Show details of a volume backup. + + :param backup_id: The ID of the backup to display. + :rtype: :class:`VolumeBackup` + """ + return self._get("/backups/%s" % backup_id, "backup") + + def list(self, detailed=True): + """Get a list of all volume backups. + + :rtype: list of :class:`VolumeBackup` + """ + if detailed is True: + return self._list("/backups/detail", "backups") + else: + return self._list("/backups", "backups") + + def delete(self, backup): + """Delete a volume backup. + + :param backup: The :class:`VolumeBackup` to delete. + """ + self._delete("/backups/%s" % base.getid(backup)) diff --git a/cinderclient/v1/volume_backups_restore.py b/cinderclient/v1/volume_backups_restore.py new file mode 100644 index 0000000..9405f12 --- /dev/null +++ b/cinderclient/v1/volume_backups_restore.py @@ -0,0 +1,43 @@ +# 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 Backups Restore interface (1.1 extension). + +This is part of the Volume Backups interface. +""" + +from cinderclient import base + + +class VolumeBackupsRestore(base.Resource): + """A Volume Backups Restore represents a restore operation.""" + def __repr__(self): + return "" % self.id + + +class VolumeBackupRestoreManager(base.ManagerWithFind): + """Manage :class:`VolumeBackupsRestore` resources.""" + resource_class = VolumeBackupsRestore + + def restore(self, backup_id, volume_id=None): + """Restore a backup to a volume. + + :param backup_id: The ID of the backup to restore. + :param volume_id: The ID of the volume to restore the backup to. + :rtype: :class:`Restore` + """ + body = {'restore': {'volume_id': volume_id}} + return self._create("/backups/%s/restore" % backup_id, + body, "restore") diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 92bbd56..9e17718 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -5,6 +5,8 @@ from cinderclient.v2 import quotas from cinderclient.v2 import volumes 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 class Client(object): @@ -39,6 +41,8 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) + self.backups = volume_backups.VolumeBackupManager(self) + self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index ee0ed23..081fe23 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -62,12 +62,16 @@ def _find_volume_snapshot(cs, snapshot): return utils.find_resource(cs.volume_snapshots, snapshot) +def _find_backup(cs, backup): + """Get a backup by ID.""" + return utils.find_resource(cs.backups, backup) + + def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) -def _translate_volume_keys(collection): - convert = [('volumeType', 'volume_type')] +def _translate_keys(collection, convert): for item in collection: keys = item.__dict__.keys() for from_key, to_key in convert: @@ -75,13 +79,14 @@ def _translate_volume_keys(collection): setattr(item, to_key, item._info[from_key]) +def _translate_volume_keys(collection): + convert = [('volumeType', 'volume_type')] + _translate_keys(collection, convert) + + def _translate_volume_snapshot_keys(collection): convert = [('volumeId', 'volume_id')] - for item in collection: - keys = 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]) + _translate_keys(collection, convert) def _extract_metadata(args): @@ -703,3 +708,78 @@ def do_upload_to_image(cs, args): args.image_name, args.container_format, args.disk_format) + + +@utils.arg('volume', metavar='', + help='ID of the volume to backup.') +@utils.arg('--container', metavar='', + help='Optional backup container name. (Default=None)', + default=None) +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--name', metavar='', + help='Optional backup name. (Default=None)', + default=None) +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--description', + metavar='', + default=None, + help='Options backup description (Default=None)') +@utils.service_type('volume') +def do_backup_create(cs, args): + """Creates a backup.""" + if args.display_name is not None: + args.name = args.display_name + + if args.display_description is not None: + args.description = args.display_description + + cs.backups.create(args.volume, + args.container, + args.name, + args.description) + + +@utils.arg('backup', metavar='', help='ID of the backup.') +@utils.service_type('volume') +def do_backup_show(cs, args): + """Show details about a backup.""" + backup = _find_backup(cs, args.backup) + info = dict() + info.update(backup._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.service_type('volume') +def do_backup_list(cs, args): + """List all the backups.""" + backups = cs.backups.list() + columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', + 'Container'] + utils.print_list(backups, columns) + + +@utils.arg('backup', metavar='', + help='ID of the backup to delete.') +@utils.service_type('volume') +def do_backup_delete(cs, args): + """Remove a backup.""" + backup = _find_backup(cs, args.backup) + backup.delete() + + +@utils.arg('backup', metavar='', + help='ID of the backup to restore.') +@utils.arg('--volume-id', metavar='', + help='Optional ID of the volume to restore to.', + default=None) +@utils.service_type('volume') +def do_backup_restore(cs, args): + """Restore a backup.""" + cs.restores.restore(args.backup, + args.volume_id) diff --git a/cinderclient/v2/volume_backups.py b/cinderclient/v2/volume_backups.py new file mode 100644 index 0000000..89056af --- /dev/null +++ b/cinderclient/v2/volume_backups.py @@ -0,0 +1,76 @@ +# 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 Backups interface (1.1 extension). +""" + +from cinderclient import base + + +class VolumeBackup(base.Resource): + """A volume backup is a block level backup of a volume.""" + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume backup.""" + return self.manager.delete(self) + + +class VolumeBackupManager(base.ManagerWithFind): + """Manage :class:`VolumeBackup` resources.""" + resource_class = VolumeBackup + + def create(self, volume_id, container=None, + name=None, description=None): + """Create a volume backup. + + :param volume_id: The ID of the volume to backup. + :param container: The name of the backup service container. + :param name: The name of the backup. + :param description: The description of the backup. + :rtype: :class:`VolumeBackup` + """ + body = {'backup': {'volume_id': volume_id, + 'container': container, + 'name': name, + 'description': description}} + return self._create('/backups', body, 'backup') + + def get(self, backup_id): + """Show details of a volume backup. + + :param backup_id: The ID of the backup to display. + :rtype: :class:`VolumeBackup` + """ + return self._get("/backups/%s" % backup_id, "backup") + + def list(self, detailed=True): + """Get a list of all volume backups. + + :rtype: list of :class:`VolumeBackup` + """ + if detailed is True: + return self._list("/backups/detail", "backups") + else: + return self._list("/backups", "backups") + + def delete(self, backup): + """Delete a volume backup. + + :param backup: The :class:`VolumeBackup` to delete. + """ + self._delete("/backups/%s" % base.getid(backup)) diff --git a/cinderclient/v2/volume_backups_restore.py b/cinderclient/v2/volume_backups_restore.py new file mode 100644 index 0000000..9405f12 --- /dev/null +++ b/cinderclient/v2/volume_backups_restore.py @@ -0,0 +1,43 @@ +# 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 Backups Restore interface (1.1 extension). + +This is part of the Volume Backups interface. +""" + +from cinderclient import base + + +class VolumeBackupsRestore(base.Resource): + """A Volume Backups Restore represents a restore operation.""" + def __repr__(self): + return "" % self.id + + +class VolumeBackupRestoreManager(base.ManagerWithFind): + """Manage :class:`VolumeBackupsRestore` resources.""" + resource_class = VolumeBackupsRestore + + def restore(self, backup_id, volume_id=None): + """Restore a backup to a volume. + + :param backup_id: The ID of the backup to restore. + :param volume_id: The ID of the volume to restore the backup to. + :rtype: :class:`Restore` + """ + body = {'restore': {'volume_id': volume_id}} + return self._create("/backups/%s/restore" % backup_id, + body, "restore") diff --git a/tests/v1/fakes.py b/tests/v1/fakes.py index ed0a640..ee4a58d 100644 --- a/tests/v1/fakes.py +++ b/tests/v1/fakes.py @@ -57,6 +57,60 @@ def _stub_snapshot(**kwargs): return snapshot +def _self_href(base_uri, tenant_id, backup_id): + return '%s/v1/%s/backups/%s' % (base_uri, tenant_id, backup_id) + + +def _bookmark_href(base_uri, tenant_id, backup_id): + return '%s/%s/backups/%s' % (base_uri, tenant_id, backup_id) + + +def _stub_backup_full(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'backup', + 'description': 'nightly backup', + 'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b', + 'container': 'volumebackups', + 'object_count': 220, + 'size': 10, + 'availability_zone': 'az1', + 'created_at': '2013-04-12T08:16:37.000000', + 'status': 'available', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_backup(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'backup', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_restore(): + return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -313,3 +367,38 @@ class FakeHTTPClient(base_client.HTTPClient): }, ] return (200, {}, {"extensions": exts, }) + + # + # VolumeBackups + # + + def get_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + return (200, {}, + {'backup': _stub_backup_full(backup1, base_uri, tenant_id)}) + + def get_backups_detail(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + backup2 = 'd09534c6-08b8-4441-9e87-8976f3a8f699' + return (200, {}, + {'backups': [ + _stub_backup_full(backup1, base_uri, tenant_id), + _stub_backup_full(backup2, base_uri, tenant_id)]}) + + def delete_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): + return (202, {}, None) + + def post_backups(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + return (202, {}, + {'backup': _stub_backup(backup1, base_uri, tenant_id)}) + + def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw): + return (200, {}, + {'restore': _stub_restore()}) diff --git a/tests/v1/test_volume_backups.py b/tests/v1/test_volume_backups.py new file mode 100644 index 0000000..4c11fce --- /dev/null +++ b/tests/v1/test_volume_backups.py @@ -0,0 +1,53 @@ +# 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 tests import utils +from tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class VolumeBackupsTest(utils.TestCase): + + def test_create(self): + cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4') + cs.assert_called('POST', '/backups') + + def test_get(self): + backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' + cs.backups.get(backup_id) + cs.assert_called('GET', '/backups/%s' % backup_id) + + def test_list(self): + cs.backups.list() + cs.assert_called('GET', '/backups/detail') + + def test_delete(self): + b = cs.backups.list()[0] + b.delete() + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62') + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + cs.backups.delete(b) + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + + def test_restore(self): + backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' + cs.restores.restore(backup_id) + cs.assert_called('POST', '/backups/%s/restore' % backup_id) diff --git a/tests/v2/fakes.py b/tests/v2/fakes.py index af11af7..038a434 100644 --- a/tests/v2/fakes.py +++ b/tests/v2/fakes.py @@ -64,6 +64,60 @@ def _stub_snapshot(**kwargs): return snapshot +def _self_href(base_uri, tenant_id, backup_id): + return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id) + + +def _bookmark_href(base_uri, tenant_id, backup_id): + return '%s/%s/backups/%s' % (base_uri, tenant_id, backup_id) + + +def _stub_backup_full(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'backup', + 'description': 'nightly backup', + 'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b', + 'container': 'volumebackups', + 'object_count': 220, + 'size': 10, + 'availability_zone': 'az1', + 'created_at': '2013-04-12T08:16:37.000000', + 'status': 'available', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_backup(id, base_uri, tenant_id): + return { + 'id': id, + 'name': 'backup', + 'links': [ + { + 'href': _self_href(base_uri, tenant_id, id), + 'rel': 'self' + }, + { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + ] + } + + +def _stub_restore(): + return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} + + class FakeClient(fakes.FakeClient, client.Client): def __init__(self, *args, **kwargs): @@ -320,3 +374,38 @@ class FakeHTTPClient(base_client.HTTPClient): }, ] return (200, {}, {"extensions": exts, }) + + # + # VolumeBackups + # + + def get_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + return (200, {}, + {'backup': _stub_backup_full(backup1, base_uri, tenant_id)}) + + def get_backups_detail(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + backup2 = 'd09534c6-08b8-4441-9e87-8976f3a8f699' + return (200, {}, + {'backups': [ + _stub_backup_full(backup1, base_uri, tenant_id), + _stub_backup_full(backup2, base_uri, tenant_id)]}) + + def delete_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): + return (202, {}, None) + + def post_backups(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' + return (202, {}, + {'backup': _stub_backup(backup1, base_uri, tenant_id)}) + + def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw): + return (200, {}, + {'restore': _stub_restore()}) diff --git a/tests/v2/test_volume_backups.py b/tests/v2/test_volume_backups.py new file mode 100644 index 0000000..083da31 --- /dev/null +++ b/tests/v2/test_volume_backups.py @@ -0,0 +1,53 @@ +# 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 tests import utils +from tests.v2 import fakes + + +cs = fakes.FakeClient() + + +class VolumeBackupsTest(utils.TestCase): + + def test_create(self): + cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4') + cs.assert_called('POST', '/backups') + + def test_get(self): + backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' + cs.backups.get(backup_id) + cs.assert_called('GET', '/backups/%s' % backup_id) + + def test_list(self): + cs.backups.list() + cs.assert_called('GET', '/backups/detail') + + def test_delete(self): + b = cs.backups.list()[0] + b.delete() + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62') + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + cs.backups.delete(b) + cs.assert_called('DELETE', + '/backups/76a17945-3c6f-435c-975b-b5685db10b62') + + def test_restore(self): + backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' + cs.restores.restore(backup_id) + cs.assert_called('POST', '/backups/%s/restore' % backup_id) From eaa04175eafd78925d33ed9ccce9035737fc2f3d Mon Sep 17 00:00:00 2001 From: John Griffith Date: Wed, 1 May 2013 09:27:57 -0600 Subject: [PATCH 011/165] Update setup.py prior to next upload to pypi. Minor updates to the setup.py file including: author and email. Change-Id: I0194a5fdaa1ea441115e6e869182e4dcb5b531e0 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a2b0693..ffefec4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ import os import setuptools -import sys from cinderclient.openstack.common import setup @@ -32,8 +31,8 @@ def read_file(file_name): setuptools.setup( name=project, version=setup.get_version(project), - author="Rackspace, based on work by Jacob Kaplan-Moss", - author_email="github@racklabs.com", + 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", @@ -43,6 +42,7 @@ setuptools.setup( 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", From f2835f4d428d3a6addba4312ada988345ed1ab89 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Wed, 1 May 2013 17:25:58 -0600 Subject: [PATCH 012/165] Update release info in index.rst. Add release notes and fix revision number mishap. Change-Id: Ia48be8462e17c900562b794fd69ba2234f6c6c03 --- doc/source/index.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 1ebe9a9..e82c824 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -27,8 +27,16 @@ Once you've configured your authentication parameters, you can run ``cinder help Release Notes ============= +1.0.4 +----- +* Added suport for backup-service commands +.. _1163546: http://bugs.launchpad.net/python-cinderclient/+bug/1163546 +.. _1161857: http://bugs.launchpad.net/python-cinderclient/+bug/1161857 +.. _1160898: http://bugs.launchpad.net/python-cinderclient/+bug/1160898 +.. _1161857: http://bugs.launchpad.net/python-cinderclient/+bug/1161857 +.. _1156994: http://bugs.launchpad.net/python-cinderclient/+bug/1156994 -1.1.0 +1.0.3 ----- * Added support for V2 Cinder API From 2ed5cdcb5f702a036be46c354b1511aa26f7c940 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 6 May 2013 18:25:26 -0500 Subject: [PATCH 013/165] 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 7b3ba730a7f3f005f359178c3509adf07cd58652 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 13 May 2013 09:54:14 +0000 Subject: [PATCH 014/165] Uploading new upstream release 1.0.4 to unstable --- debian/changelog | 7 +++++++ debian/gbp.conf | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 0669b4b..6c2fb82 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +python-cinderclient (1:1.0.4-1) unstable; urgency=low + + * Uploading to unstable. + * New upstream release. + + -- Thomas Goirand Mon, 13 May 2013 09:53:36 +0000 + python-cinderclient (1:1.0.2-1) experimental; urgency=low * New upstream release. diff --git a/debian/gbp.conf b/debian/gbp.conf index fe85503..ccf8702 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,6 +1,6 @@ [DEFAULT] upstream-branch = master -debian-branch = debian/experimental +debian-branch = debian/unstable upstream-tag = %(version)s compression = xz From 1f14d086a66785b980cc8a1ea43dd4fb97128fc8 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 13 May 2013 09:55:47 +0000 Subject: [PATCH 015/165] Added export OSLO_PACKAGE_VERSION=$(VERSION) in debian/rules --- debian/changelog | 1 + debian/rules | 2 ++ 2 files changed, 3 insertions(+) diff --git a/debian/changelog b/debian/changelog index 6c2fb82..614aa61 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ python-cinderclient (1:1.0.4-1) unstable; urgency=low * Uploading to unstable. * New upstream release. + * Added export OSLO_PACKAGE_VERSION=$(VERSION) in debian/rules. -- Thomas Goirand Mon, 13 May 2013 09:53:36 +0000 diff --git a/debian/rules b/debian/rules index 2369969..20b724a 100755 --- a/debian/rules +++ b/debian/rules @@ -5,6 +5,8 @@ include /usr/share/openstack-pkg-tools/pkgos.make +export OSLO_PACKAGE_VERSION=$(VERSION) + override_dh_auto_clean: echo $(VERSION) > cinderclient/versioninfo dh_auto_clean From 22356371163a96f243e524e773eb6add17a12c75 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 13 May 2013 10:00:02 +0000 Subject: [PATCH 016/165] Fixed the launch of unit tests. --- debian/changelog | 1 + debian/rules | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 614aa61..71e4e55 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,6 +3,7 @@ python-cinderclient (1:1.0.4-1) unstable; urgency=low * Uploading to unstable. * New upstream release. * Added export OSLO_PACKAGE_VERSION=$(VERSION) in debian/rules. + * Fixed the launch of unit tests. -- Thomas Goirand Mon, 13 May 2013 09:53:36 +0000 diff --git a/debian/rules b/debian/rules index 20b724a..2095940 100755 --- a/debian/rules +++ b/debian/rules @@ -23,5 +23,9 @@ override_dh_install: %: dh $@ --with python2 +ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) override_dh_auto_test: - ./run_tests.sh -N -P + for i in `pyversions -s` ; do \ + $$i -m subunit.run discover -t ./ ./tests ; \ + done +endif From a122a76b3d20573ca3082ecfcaa6499e8fbba400 Mon Sep 17 00:00:00 2001 From: Alexey Ovchinnikov Date: Tue, 14 May 2013 17:07:29 +0400 Subject: [PATCH 017/165] 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 018/165] 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 019/165] 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 4f5b3f1dd48f559e10920c8992867a00225f63e9 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sat, 18 May 2013 01:27:23 +0800 Subject: [PATCH 020/165] Fix the setuptools_git-1.0b1-py2.7.egg virtualenv thing. --- debian/control | 1 + debian/rules | 1 + 2 files changed, 2 insertions(+) diff --git a/debian/control b/debian/control index 4fe3ff6..ec207ba 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~) Build-Depends-Indep: openstack-pkg-tools, python-setuptools, + python-setuptools-git, python-nose, python-httplib2, python-prettytable, diff --git a/debian/rules b/debian/rules index 2095940..26e80ab 100755 --- a/debian/rules +++ b/debian/rules @@ -11,6 +11,7 @@ override_dh_auto_clean: echo $(VERSION) > cinderclient/versioninfo dh_auto_clean rm -f cinderclient/versioninfo + rm -rf setuptools_git-*.egg override_dh_auto_configure: echo $(VERSION) > cinderclient/versioninfo From 5c8c2d27c89ef71e8c37498fd38b41dfe411afc3 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sat, 18 May 2013 01:28:23 +0800 Subject: [PATCH 021/165] Added rm -rf build --- debian/rules | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/rules b/debian/rules index 26e80ab..a5e5ece 100755 --- a/debian/rules +++ b/debian/rules @@ -12,6 +12,7 @@ override_dh_auto_clean: dh_auto_clean rm -f cinderclient/versioninfo rm -rf setuptools_git-*.egg + rm -rf build override_dh_auto_configure: echo $(VERSION) > cinderclient/versioninfo From 216c3f046022ad1d389e88d0b7bcb425115a652b Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sat, 18 May 2013 01:30:43 +0800 Subject: [PATCH 022/165] Added missing build-depends: python-subunit --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index ec207ba..6c1778d 100644 --- a/debian/control +++ b/debian/control @@ -18,6 +18,7 @@ Build-Depends-Indep: openstack-pkg-tools, python-mock, pep8, python-sphinx, + python-subunit, python-unittest2 Standards-Version: 3.9.3 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary From 7f943fd784d6092ceb5bd5d4fc3449e0353b07c8 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Fri, 17 May 2013 17:40:39 +0000 Subject: [PATCH 023/165] Edited debian/changelog. --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 71e4e55..9a440e7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.4-2) unstable; urgency=low + + * Added missing build-dependencies: python-setputools-git and python-subunit. + + -- Thomas Goirand Fri, 17 May 2013 17:39:51 +0000 + python-cinderclient (1:1.0.4-1) unstable; urgency=low * Uploading to unstable. From 24b4039bae557a8f29be783c3fdd3f9f148c4b0e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 09:09:35 -0700 Subject: [PATCH 024/165] 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 025/165] 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 026/165] 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 027/165] 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 028/165] 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 029/165] 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 030/165] 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 031/165] 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 51c062f9ccb2ad68080952714845adbb5c2a816c Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Thu, 30 May 2013 13:42:50 +0800 Subject: [PATCH 032/165] Ran wrap-and-sort. --- debian/changelog | 6 ++++++ debian/control | 34 +++++++++++++++++----------------- debian/copyright | 1 - 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/debian/changelog b/debian/changelog index 9a440e7..91d2d91 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.4-3) UNRELEASED; urgency=low + + * Ran wrap-and-sort. + + -- Thomas Goirand Thu, 30 May 2013 13:42:32 +0800 + python-cinderclient (1:1.0.4-2) unstable; urgency=low * Added missing build-dependencies: python-setputools-git and python-subunit. diff --git a/debian/control b/debian/control index 6c1778d..c9a8a72 100644 --- a/debian/control +++ b/debian/control @@ -6,20 +6,19 @@ Uploaders: Julien Danjou , Thomas Goirand , Ghe Rivero , Mehdi Abaakouk -Build-Depends: debhelper (>= 9), - python-all (>= 2.6.6-3~) +Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~) Build-Depends-Indep: openstack-pkg-tools, - python-setuptools, - python-setuptools-git, - python-nose, - python-httplib2, - python-prettytable, - python-simplejson, - python-mock, - pep8, - python-sphinx, - python-subunit, - python-unittest2 + pep8, + python-httplib2, + python-mock, + python-nose, + python-prettytable, + python-setuptools, + python-setuptools-git, + python-simplejson, + python-sphinx, + python-subunit, + python-unittest2 Standards-Version: 3.9.3 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary Vcs-Git: git://anonscm.debian.org/openstack/python-cinderclient.git @@ -28,10 +27,11 @@ Homepage: https://github.com/openstack/python-cinderclient Package: python-cinderclient Architecture: all Pre-Depends: dpkg (>= 1.15.6~) -Depends: ${python:Depends}, ${misc:Depends}, - python-httplib2, - python-prettytable, - python-simplejson +Depends: python-httplib2, + python-prettytable, + python-simplejson, + ${misc:Depends}, + ${python:Depends} Provides: ${python:Provides} Description: python bindings to the OpenStack Volume API Cinder is a block storage as service system for the Openstack cloud computing diff --git a/debian/copyright b/debian/copyright index 6d4b2f8..bcf5a3b 100644 --- a/debian/copyright +++ b/debian/copyright @@ -42,4 +42,3 @@ License: GPL-2+ . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". - From 2e58e73a0cdfae43ca206abcc1de0743324d8b68 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Fri, 31 May 2013 13:42:17 -0600 Subject: [PATCH 033/165] 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 034/165] 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 035/165] 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 036/165] 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 037/165] 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 038/165] 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 039/165] 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 040/165] 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 041/165] 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 042/165] 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 043/165] 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 044/165] 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 045/165] 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 046/165] 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 047/165] 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 048/165] 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 049/165] 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 050/165] 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 051/165] 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 052/165] 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 053/165] 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 054/165] 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 055/165] 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 056/165] 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 057/165] 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 058/165] 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 059/165] 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 060/165] 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 061/165] 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 062/165] 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 063/165] 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 064/165] 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 065/165] 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 066/165] 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 067/165] 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 fd409626fa0b1bac5cc1c97e59d7f5b13a5a890f Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Thu, 1 Aug 2013 18:22:55 +0800 Subject: [PATCH 068/165] Add timeout parameter in requests Fix bug #1207260 Change-Id: I0f57a9b27c2da2521adb6aebfe3fa072c6b56808 --- cinderclient/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cinderclient/client.py b/cinderclient/client.py index 857f80a..958a9eb 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -79,6 +79,7 @@ class HTTPClient(object): self.auth_token = None self.proxy_token = proxy_token self.proxy_tenant_id = proxy_tenant_id + self.timeout = timeout if insecure: self.verify_cert = False @@ -133,6 +134,8 @@ class HTTPClient(object): kwargs['data'] = json.dumps(kwargs['body']) del kwargs['body'] + if self.timeout: + kwargs.setdefault('timeout', self.timeout) self.http_log_req((url, method,), kwargs) resp = requests.request( method, From aeab4e4335a94db6ca68950110bd9dd0f9d94b4f Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sat, 3 Aug 2013 00:09:23 +0000 Subject: [PATCH 069/165] 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 070/165] 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 c95e59f51041dc01812d34a6b6e0765dc8c4d034 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 6 Aug 2013 11:59:34 +0200 Subject: [PATCH 071/165] convert third-party exception to ConnectionError fixes bug #1207635 Change-Id: I37da522e812286e72706409b8a6d4652515f720f --- cinderclient/client.py | 3 ++- cinderclient/exceptions.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 857f80a..792ffd5 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -192,7 +192,8 @@ class HTTPClient(object): except requests.exceptions.ConnectionError as e: # Catch a connection refused from requests.request self._logger.debug("Connection refused: %s" % e) - raise + msg = 'Unable to establish connection: %s' % e + raise exceptions.ConnectionError(msg) self._logger.debug( "Failed attempt(%s of %s), retrying in %s seconds" % (attempts, self.retries, backoff)) diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index 0c52c9c..9d3e0bc 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -39,6 +39,11 @@ class EndpointNotFound(Exception): pass +class ConnectionError(Exception): + """Could not open a connection to the API service.""" + pass + + class AmbiguousEndpoints(Exception): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): From 61006510007f7b23a25bf0bfcd8ad7d740def9e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Aug 2013 13:36:45 -0300 Subject: [PATCH 072/165] 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 073/165] 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 From fdf65aee922eea5a8bb047b55f5d5ade7cbe759e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 3 Aug 2013 01:54:45 +0000 Subject: [PATCH 074/165] python3: Fix tox requirements Update tox.ini to no to install distribute. Also bump testrepository to a newerver version since it fixes some python3 compat issues. Change-Id: I735dc28cdb94e3376b391d11220c1ba90ddb8b2e Signed-off-by: Chuck Short --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index ca047b7..65bcb21 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +distribute = False envlist = py26,py27,py33,pep8 [testenv] From 2465bb5d7ecbe0b1ca0158fe02b5c3fc0dde508e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2013 19:03:55 -0300 Subject: [PATCH 075/165] Updated from global requirements Change-Id: I2e2bd3a38458e1307bcc0410da74dc76c0a5987a --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 068a2f9..3a14372 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -pbr>=0.5.16,<0.6 +pbr>=0.5.21,<1.0 argparse PrettyTable>=0.6,<0.8 -requests>=1.1,<1.2.3 +requests>=1.1 simplejson>=2.0.9 six diff --git a/setup.py b/setup.py index 15f4e9d..2a0786a 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.20'], + setup_requires=['pbr>=0.5.21,<1.0'], pbr=True) From 34c7c8c2e2c87a6203ec9f4d8ac67fef816eb868 Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Wed, 7 Aug 2013 10:01:26 -0700 Subject: [PATCH 076/165] Add support for multiple cinder endpoints Before v1 and v2 were set to the same service_type, so you could only have one set at a time. This assumes the catalog is setup for v1 service_type 'volume' and v2 service_type 'volumev2'. For backwards compatibility, we will allow v2 to be setup with service_type 'volume' for existing installations. Change-Id: Ife6d2cdb12d894b84ea3b276767fb93d487355d5 --- cinderclient/service_catalog.py | 21 ++-- cinderclient/shell.py | 6 +- cinderclient/tests/test_service_catalog.py | 137 ++++++++++++++++++++- cinderclient/tests/v1/test_auth.py | 57 ++++----- cinderclient/tests/v2/test_auth.py | 57 ++++----- cinderclient/v2/client.py | 2 +- cinderclient/v2/shell.py | 84 ++++++------- 7 files changed, 254 insertions(+), 110 deletions(-) diff --git a/cinderclient/service_catalog.py b/cinderclient/service_catalog.py index 2be335a..09ef59c 100644 --- a/cinderclient/service_catalog.py +++ b/cinderclient/service_catalog.py @@ -52,15 +52,22 @@ class ServiceCatalog(object): catalog = self.catalog['access']['serviceCatalog'] for service in catalog: - if service.get("type") != service_type: + + # NOTE(thingee): For backwards compatibility, if they have v2 + # enabled and the service_type is set to 'volume', go ahead and + # accept that. + skip_service_type_check = False + if service_type == 'volumev2' and service['type'] == 'volume': + version = service['endpoints'][0]['publicURL'].split('/')[3] + if version == 'v2': + skip_service_type_check = True + + if (not skip_service_type_check + and service.get("type") != service_type): continue - if (service_name and service_type == 'compute' and - service.get('name') != service_name): - continue - - if (volume_service_name and service_type == 'volume' and - service.get('name') != volume_service_name): + if (volume_service_name and service_type in ('volume', 'volumev2') + and service.get('name') != volume_service_name): continue endpoints = service['endpoints'] diff --git a/cinderclient/shell.py b/cinderclient/shell.py index d067996..049324c 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -41,7 +41,7 @@ from cinderclient.v2 import shell as shell_v2 DEFAULT_OS_VOLUME_API_VERSION = "1" DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL' -DEFAULT_CINDER_SERVICE_TYPE = 'compute' +DEFAULT_CINDER_SERVICE_TYPE = 'volume' logger = logging.getLogger(__name__) @@ -145,7 +145,7 @@ class OpenStackCinderShell(object): parser.add_argument('--service-type', metavar='', - help='Defaults to compute for most actions') + help='Defaults to volume for most actions') parser.add_argument('--service_type', help=argparse.SUPPRESS) @@ -173,7 +173,7 @@ class OpenStackCinderShell(object): help=argparse.SUPPRESS) parser.add_argument('--os-volume-api-version', - metavar='', + metavar='', default=utils.env('OS_VOLUME_API_VERSION', default=DEFAULT_OS_VOLUME_API_VERSION), help='Accepts 1 or 2,defaults ' diff --git a/cinderclient/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py index 1025bd5..1055962 100644 --- a/cinderclient/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -72,7 +72,7 @@ SERVICE_CATALOG = { "endpoints_links": [], }, { - "name": "Nova Volumes", + "name": "Cinder Volume Service", "type": "volume", "endpoints": [ { @@ -101,6 +101,128 @@ SERVICE_CATALOG = { }, ], }, + { + "name": "Cinder Volume Service V2", + "type": "volumev2", + "endpoints": [ + { + "tenantId": "1", + "publicURL": "https://volume1.host/v2/1234", + "internalURL": "https://volume1.host/v2/1234", + "region": "South", + "versionId": "2.0", + "versionInfo": "uri", + "versionList": "uri" + }, + { + "tenantId": "2", + "publicURL": "https://volume1.host/v2/3456", + "internalURL": "https://volume1.host/v2/3456", + "region": "South", + "versionId": "1.1", + "versionInfo": "https://volume1.host/v2/", + "versionList": "https://volume1.host/" + }, + ], + "endpoints_links": [ + { + "rel": "next", + "href": "https://identity1.host/v2.0/endpoints" + }, + ], + }, + ], + "serviceCatalog_links": [ + { + "rel": "next", + "href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar", + }, + ], + }, +} + +SERVICE_COMPATIBILITY_CATALOG = { + "access": { + "token": { + "id": "ab48a9efdfedb23ty3494", + "expires": "2010-11-01T03:32:15-05:00", + "tenant": { + "id": "345", + "name": "My Project" + } + }, + "user": { + "id": "123", + "name": "jqsmith", + "roles": [ + { + "id": "234", + "name": "compute:admin", + }, + { + "id": "235", + "name": "object-store:admin", + "tenantId": "1", + } + ], + "roles_links": [], + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "tenantId": "1", + "publicURL": "https://compute1.host/v1/1234", + "internalURL": "https://compute1.host/v1/1234", + "region": "North", + "versionId": "1.0", + "versionInfo": "https://compute1.host/v1/", + "versionList": "https://compute1.host/" + }, + { + "tenantId": "2", + "publicURL": "https://compute1.host/v1/3456", + "internalURL": "https://compute1.host/v1/3456", + "region": "North", + "versionId": "1.1", + "versionInfo": "https://compute1.host/v1/", + "versionList": "https://compute1.host/" + }, + ], + "endpoints_links": [], + }, + { + "name": "Cinder Volume Service V2", + "type": "volume", + "endpoints": [ + { + "tenantId": "1", + "publicURL": "https://volume1.host/v2/1234", + "internalURL": "https://volume1.host/v2/1234", + "region": "South", + "versionId": "2.0", + "versionInfo": "uri", + "versionList": "uri" + }, + { + "tenantId": "2", + "publicURL": "https://volume1.host/v2/3456", + "internalURL": "https://volume1.host/v2/3456", + "region": "South", + "versionId": "1.1", + "versionInfo": "https://volume1.host/v2/", + "versionList": "https://volume1.host/" + }, + ], + "endpoints_links": [ + { + "rel": "next", + "href": "https://identity1.host/v2.0/endpoints" + }, + ], + }, ], "serviceCatalog_links": [ { @@ -136,5 +258,18 @@ class ServiceCatalogTest(utils.TestCase): self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), "https://volume1.host/v1/3456") + self.assertEquals(sc.url_for('tenantId', '2', service_type='volumev2'), + "https://volume1.host/v2/3456") + self.assertEquals(sc.url_for('tenantId', '2', service_type='volumev2'), + "https://volume1.host/v2/3456") + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", "North", service_type='volume') + + def test_compatibility_service_type(self): + sc = service_catalog.ServiceCatalog(SERVICE_COMPATIBILITY_CATALOG) + + self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'), + "https://volume1.host/v2/1234") + self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), + "https://volume1.host/v2/3456") diff --git a/cinderclient/tests/v1/test_auth.py b/cinderclient/tests/v1/test_auth.py index b681dc7..fa9a126 100644 --- a/cinderclient/tests/v1/test_auth.py +++ b/cinderclient/tests/v1/test_auth.py @@ -24,7 +24,7 @@ from cinderclient.tests import utils class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + "http://localhost:8776/v1", service_type='volume') resp = { "access": { "token": { @@ -33,13 +33,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": "volume", "endpoints": [ { "region": "RegionOne", - "adminURL": "http://localhost:8774/v1", - "internalURL": "http://localhost:8774/v1", - "publicURL": "http://localhost:8774/v1/", + "adminURL": "http://localhost:8776/v1", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1", }, ], }, @@ -89,8 +89,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): test_auth_call() def test_authenticate_tenant_id(self): - cs = client.Client("username", "password", auth_url="auth_url/v2.0", - tenant_id='tenant_id', service_type='compute') + cs = client.Client("username", "password", + auth_url="http://localhost:8776/v1", + tenant_id='tenant_id', service_type='volume') resp = { "access": { "token": { @@ -105,13 +106,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": "volume", "endpoints": [ { "region": "RegionOne", - "adminURL": "http://localhost:8774/v1", - "internalURL": "http://localhost:8774/v1", - "publicURL": "http://localhost:8774/v1/", + "adminURL": "http://localhost:8776/v1", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1", }, ], }, @@ -164,7 +165,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0") + "http://localhost:8776/v1") resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -181,7 +182,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v1", service_type='compute') + "http://localhost:8776/v1", service_type='volume') dict_correct_response = { "access": { "token": { @@ -190,13 +191,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": "volume", "endpoints": [ { - "adminURL": "http://localhost:8774/v1", + "adminURL": "http://localhost:8776/v1", "region": "RegionOne", - "internalURL": "http://localhost:8774/v1", - "publicURL": "http://localhost:8774/v1/", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1/", }, ], }, @@ -265,7 +266,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + "http://localhost:8776/v1", service_type='volume') resp = { "access": { "token": { @@ -274,25 +275,25 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "adminURL": "http://localhost:8774/v1", - "type": "compute", - "name": "Compute CLoud", + "adminURL": "http://localhost:8776/v1", + "type": "volume", + "name": "Cinder Volume Service", "endpoints": [ { "region": "RegionOne", - "internalURL": "http://localhost:8774/v1", - "publicURL": "http://localhost:8774/v1/", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1", }, ], }, { - "adminURL": "http://localhost:8774/v1", - "type": "compute", - "name": "Hyper-compute Cloud", + "adminURL": "http://localhost:8776/v1", + "type": "volume", + "name": "Cinder Volume Cloud Service", "endpoints": [ { - "internalURL": "http://localhost:8774/v1", - "publicURL": "http://localhost:8774/v1/", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1", }, ], }, diff --git a/cinderclient/tests/v2/test_auth.py b/cinderclient/tests/v2/test_auth.py index 89dd18f..2ae3eed 100644 --- a/cinderclient/tests/v2/test_auth.py +++ b/cinderclient/tests/v2/test_auth.py @@ -27,7 +27,7 @@ from cinderclient.tests import utils class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + "http://localhost:8776/v2", service_type='volumev2') resp = { "access": { "token": { @@ -36,13 +36,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": "volumev2", "endpoints": [ { "region": "RegionOne", - "adminURL": "http://localhost:8774/v2", - "internalURL": "http://localhost:8774/v2", - "publicURL": "http://localhost:8774/v2/", + "adminURL": "http://localhost:8776/v2", + "internalURL": "http://localhost:8776/v2", + "publicURL": "http://localhost:8776/v2", }, ], }, @@ -92,8 +92,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): test_auth_call() def test_authenticate_tenant_id(self): - cs = client.Client("username", "password", auth_url="auth_url/v2.0", - tenant_id='tenant_id', service_type='compute') + cs = client.Client("username", "password", + auth_url="http://localhost:8776/v2", + tenant_id='tenant_id', service_type='volumev2') resp = { "access": { "token": { @@ -108,13 +109,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": 'volumev2', "endpoints": [ { "region": "RegionOne", - "adminURL": "http://localhost:8774/v2", - "internalURL": "http://localhost:8774/v2", - "publicURL": "http://localhost:8774/v2/", + "adminURL": "http://localhost:8776/v2", + "internalURL": "http://localhost:8776/v2", + "publicURL": "http://localhost:8776/v2", }, ], }, @@ -167,7 +168,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0") + "http://localhost:8776/v2") resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -184,7 +185,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2", service_type='compute') + "http://localhost:8776/v2", service_type='volumev2') dict_correct_response = { "access": { "token": { @@ -193,13 +194,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "type": "compute", + "type": "volumev2", "endpoints": [ { - "adminURL": "http://localhost:8774/v2", + "adminURL": "http://localhost:8776/v2", "region": "RegionOne", - "internalURL": "http://localhost:8774/v2", - "publicURL": "http://localhost:8774/v2/", + "internalURL": "http://localhost:8776/v2", + "publicURL": "http://localhost:8776/v2/", }, ], }, @@ -268,7 +269,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + "http://localhost:8776/v2", service_type='volumev2') resp = { "access": { "token": { @@ -277,25 +278,25 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): }, "serviceCatalog": [ { - "adminURL": "http://localhost:8774/v2", - "type": "compute", - "name": "Compute CLoud", + "adminURL": "http://localhost:8776/v1", + "type": "volumev2", + "name": "Cinder Volume Service", "endpoints": [ { "region": "RegionOne", - "internalURL": "http://localhost:8774/v2", - "publicURL": "http://localhost:8774/v2/", + "internalURL": "http://localhost:8776/v1", + "publicURL": "http://localhost:8776/v1", }, ], }, { - "adminURL": "http://localhost:8774/v2", - "type": "compute", - "name": "Hyper-compute Cloud", + "adminURL": "http://localhost:8776/v2", + "type": "volumev2", + "name": "Cinder Volume V2", "endpoints": [ { - "internalURL": "http://localhost:8774/v2", - "publicURL": "http://localhost:8774/v2/", + "internalURL": "http://localhost:8776/v2", + "publicURL": "http://localhost:8776/v2", }, ], }, diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index fea400f..31781a8 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -44,7 +44,7 @@ class Client(object): insecure=False, timeout=None, tenant_id=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, - service_type='volume', service_name=None, + service_type='volumev2', service_name=None, volume_service_name=None, retries=None, http_log_debug=False, cacert=None): diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 53f4c04..a1c1f22 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -146,7 +146,7 @@ def _extract_metadata(args): metavar='', default=None, help='Filter results by status') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_list(cs, args): """List all the volumes.""" # NOTE(thingee): Backwards-compatibility with v1 args @@ -174,7 +174,7 @@ def do_list(cs, args): @utils.arg('volume', metavar='', help='ID of the volume.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_show(cs, args): """Show details about a volume.""" info = dict() @@ -247,7 +247,7 @@ def do_show(cs, args): action='append', default=[], help='Scheduler hint like in nova') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_create(cs, args): """Add a new volume.""" # NOTE(thingee): Backwards-compatibility with v1 args @@ -298,7 +298,7 @@ def do_create(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to delete.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_delete(cs, args): """Remove a volume.""" volume = _find_volume(cs, args.volume) @@ -308,7 +308,7 @@ def do_delete(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to delete.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_force_delete(cs, args): """Attempt forced removal of a volume, regardless of its state.""" volume = _find_volume(cs, args.volume) @@ -320,7 +320,7 @@ def do_force_delete(cs, args): 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') +@utils.service_type('volumev2') def do_reset_state(cs, args): """Explicitly update the state of a volume.""" volume = _find_volume(cs, args.volume) @@ -341,7 +341,7 @@ def do_reset_state(cs, args): help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_rename(cs, args): """Rename a volume.""" kwargs = {} @@ -369,7 +369,7 @@ def do_rename(cs, args): action='append', default=[], help='Metadata to set/unset (only key is necessary on unset)') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_metadata(cs, args): """Set or Delete metadata on a volume.""" volume = _find_volume(cs, args.volume) @@ -412,7 +412,7 @@ def do_metadata(cs, args): help='Filter results by volume-id') @utils.arg('--volume_id', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_snapshot_list(cs, args): """List all the snapshots.""" all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) @@ -436,7 +436,7 @@ def do_snapshot_list(cs, args): @utils.arg('snapshot', metavar='', help='ID of the snapshot.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_snapshot_show(cs, args): """Show details about a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -468,7 +468,7 @@ def do_snapshot_show(cs, args): help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_snapshot_create(cs, args): """Add a new snapshot.""" if args.display_name is not None: @@ -487,7 +487,7 @@ def do_snapshot_create(cs, args): @utils.arg('snapshot-id', metavar='', help='ID of the snapshot to delete.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_snapshot_delete(cs, args): """Remove a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot_id) @@ -504,7 +504,7 @@ def do_snapshot_delete(cs, args): help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_snapshot_rename(cs, args): """Rename a snapshot.""" kwargs = {} @@ -528,7 +528,7 @@ def do_snapshot_rename(cs, args): 'Options include available, error, creating, ' 'deleting, error_deleting. If no state is provided, ' 'available will be used.')) -@utils.service_type('snapshot') +@utils.service_type('volumev2') def do_snapshot_reset_state(cs, args): """Explicitly update the state of a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -544,14 +544,14 @@ def _print_type_and_extra_specs_list(vtypes): utils.print_list(vtypes, ['ID', 'Name', 'extra_specs'], formatters) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_type_list(cs, args): """Print a list of available 'volume types'.""" vtypes = cs.volume_types.list() _print_volume_type_list(vtypes) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_extra_specs_list(cs, args): """Print a list of current 'volume types and extra specs' (Admin Only).""" vtypes = cs.volume_types.list() @@ -561,7 +561,7 @@ def do_extra_specs_list(cs, args): @utils.arg('name', metavar='', help="Name of the new volume type") -@utils.service_type('volume') +@utils.service_type('volumev2') def do_type_create(cs, args): """Create a new volume type.""" vtype = cs.volume_types.create(args.name) @@ -571,7 +571,7 @@ def do_type_create(cs, args): @utils.arg('id', metavar='', help="Unique ID of the volume type to delete") -@utils.service_type('volume') +@utils.service_type('volumev2') def do_type_delete(cs, args): """Delete a specific volume type.""" cs.volume_types.delete(args.id) @@ -590,7 +590,7 @@ def do_type_delete(cs, args): action='append', default=[], help='Extra_specs to set/unset (only key is necessary on unset)') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_type_key(cs, args): "Set or unset extra_spec for a volume type.""" vtype = _find_volume_type(cs, args.vtype) @@ -648,7 +648,7 @@ def _quota_update(manager, identifier, args): @utils.arg('tenant', metavar='', help='UUID of tenant to list the quotas for.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_quota_show(cs, args): """List the quotas for a tenant.""" @@ -658,7 +658,7 @@ def do_quota_show(cs, args): @utils.arg('tenant', metavar='', help='UUID of tenant to list the default quotas for.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" @@ -684,7 +684,7 @@ def do_quota_defaults(cs, args): metavar='', default=None, help='Volume type (Optional, Default=None)') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -694,7 +694,7 @@ def do_quota_update(cs, args): @utils.arg('class_name', metavar='', help='Name of quota class to list the quotas for.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_quota_class_show(cs, args): """List the quotas for a quota class.""" @@ -720,14 +720,14 @@ def do_quota_class_show(cs, args): metavar='', default=None, help='Volume type (Optional, Default=None)') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" _quota_update(cs.quota_classes, args.class_name, args) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" limits = cs.limits.get().absolute @@ -735,7 +735,7 @@ def do_absolute_limits(cs, args): utils.print_list(limits, columns) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_rate_limits(cs, args): """Print a list of rate limits for a user""" limits = cs.limits.get().rate @@ -783,7 +783,7 @@ def _find_volume_type(cs, vtype): help='Name for created image') @utils.arg('--image_name', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_upload_to_image(cs, args): """Upload volume to image service as image.""" volume = _find_volume(cs, args.volume_id) @@ -809,7 +809,7 @@ def do_upload_to_image(cs, args): metavar='', default=None, help='Options backup description (Default=None)') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_backup_create(cs, args): """Creates a backup.""" if args.display_name is not None: @@ -825,7 +825,7 @@ def do_backup_create(cs, args): @utils.arg('backup', metavar='', help='ID of the backup.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_backup_show(cs, args): """Show details about a backup.""" backup = _find_backup(cs, args.backup) @@ -836,7 +836,7 @@ def do_backup_show(cs, args): utils.print_dict(info) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_backup_list(cs, args): """List all the backups.""" backups = cs.backups.list() @@ -847,7 +847,7 @@ def do_backup_list(cs, args): @utils.arg('backup', metavar='', help='ID of the backup to delete.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_backup_delete(cs, args): """Remove a backup.""" backup = _find_backup(cs, args.backup) @@ -859,7 +859,7 @@ def do_backup_delete(cs, args): @utils.arg('--volume-id', metavar='', help='Optional ID of the volume to restore to.', default=None) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_backup_restore(cs, args): """Restore a backup.""" cs.restores.restore(args.backup, @@ -874,7 +874,7 @@ def do_backup_restore(cs, args): help='Optional transfer name. (Default=None)') @utils.arg('--display-name', help=argparse.SUPPRESS) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_transfer_create(cs, args): """Creates a volume transfer.""" if args.display_name is not None: @@ -891,7 +891,7 @@ def do_transfer_create(cs, args): @utils.arg('transfer', metavar='', help='ID of the transfer to delete.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_transfer_delete(cs, args): """Undo a transfer.""" transfer = _find_transfer(cs, args.transfer) @@ -902,7 +902,7 @@ def do_transfer_delete(cs, args): help='ID of the transfer to accept.') @utils.arg('auth_key', metavar='', help='Auth key of the transfer to accept.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_transfer_accept(cs, args): """Accepts a volume transfer.""" transfer = cs.transfers.accept(args.transfer, args.auth_key) @@ -913,7 +913,7 @@ def do_transfer_accept(cs, args): utils.print_dict(info) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_transfer_list(cs, args): """List all the transfers.""" transfers = cs.transfers.list() @@ -923,7 +923,7 @@ def do_transfer_list(cs, args): @utils.arg('transfer', metavar='', help='ID of the transfer to accept.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_transfer_show(cs, args): """Show details about a transfer.""" transfer = _find_transfer(cs, args.transfer) @@ -939,7 +939,7 @@ def do_transfer_show(cs, args): metavar='', type=int, help='New size of volume in GB') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" volume = _find_volume(cs, args.volume) @@ -950,7 +950,7 @@ def do_extend(cs, args): help='Name of host.') @utils.arg('--binary', metavar='', default=None, help='Service binary.') -@utils.service_type('volume') +@utils.service_type('volumev2') 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) @@ -960,7 +960,7 @@ def do_service_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('binary', metavar='', help='Service binary.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_service_enable(cs, args): """Enable the service.""" cs.services.enable(args.host, args.binary) @@ -968,7 +968,7 @@ def do_service_enable(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('binary', metavar='', help='Service binary.') -@utils.service_type('volume') +@utils.service_type('volumev2') def do_service_disable(cs, args): """Disable the service.""" cs.services.disable(args.host, args.binary) @@ -1016,7 +1016,7 @@ def _treeizeAvailabilityZone(zone): return result -@utils.service_type('volume') +@utils.service_type('volumev2') def do_availability_zone_list(cs, _args): """List all the availability zones.""" try: From e19c1ccb3a0a3d179b86e75b2a58e22867d0e922 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 9 Aug 2013 15:20:47 +0000 Subject: [PATCH 077/165] Add missing babel dependency Add missing babel dependency. Change-Id: I9d9c6fe84ada1fa6c5cfb10052f76689a2c84206 Signed-off-by: Chuck Short --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 068a2f9..9ee909d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ argparse PrettyTable>=0.6,<0.8 requests>=1.1,<1.2.3 simplejson>=2.0.9 +Babel>=0.9.6 six From 219798536d47ce23c73a30894b3d3eb69574f7ad Mon Sep 17 00:00:00 2001 From: Seif Lotfy Date: Sat, 10 Aug 2013 22:00:01 +0000 Subject: [PATCH 078/165] Add print for "backup-create" command For convenience reasons backup-create should print out metadata Fixes bug: 1210874 Change-Id: I327aaadb3b82c2073cec5807aa429c4ffac6ee0f --- cinderclient/v1/shell.py | 16 ++++++++++++---- cinderclient/v2/shell.py | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 6c5d266..111a177 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -731,10 +731,18 @@ def do_upload_to_image(cs, args): @utils.service_type('volume') def do_backup_create(cs, args): """Creates a backup.""" - cs.backups.create(args.volume, - args.container, - args.display_name, - args.display_description) + backup = cs.backups.create(args.volume, + args.container, + args.display_name, + args.display_description) + + info = {"volume_id": args.volume} + info.update(backup._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) @utils.arg('backup', metavar='', help='ID of the backup.') diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index a1c1f22..8006b84 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -818,10 +818,18 @@ def do_backup_create(cs, args): if args.display_description is not None: args.description = args.display_description - cs.backups.create(args.volume, - args.container, - args.name, - args.description) + backup = cs.backups.create(args.volume, + args.container, + args.name, + args.description) + + info = {"volume_id": args.volume} + info.update(backup._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) @utils.arg('backup', metavar='', help='ID of the backup.') From b7297f3052f74f8379b854f78da49b8c05fd393c Mon Sep 17 00:00:00 2001 From: Seif Lotfy Date: Sat, 10 Aug 2013 22:31:36 +0000 Subject: [PATCH 079/165] Add commandline option --metadata for cinder list Fixes bug: 1203471 Change-Id: I8d0bd839ea467f8995e1588ec51a5590c8b80d69 --- cinderclient/v1/shell.py | 8 ++++++++ cinderclient/v2/shell.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 6c5d266..0a999c7 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -154,6 +154,13 @@ def _extract_metadata(args): metavar='', default=None, help='Filter results by status') +@utils.arg( + '--metadata', + type=str, + nargs='*', + metavar='', + help='Filter results by metadata', + default=None) @utils.service_type('volume') def do_list(cs, args): """List all the volumes.""" @@ -162,6 +169,7 @@ def do_list(cs, args): 'all_tenants': all_tenants, 'display_name': args.display_name, 'status': args.status, + 'metadata': _extract_metadata(args) if args.metadata else None, } volumes = cs.volumes.list(search_opts=search_opts) _translate_volume_keys(volumes) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index a1c1f22..b15a363 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -146,6 +146,12 @@ def _extract_metadata(args): metavar='', default=None, help='Filter results by status') +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + help='Filter results by metadata', + default=None) @utils.service_type('volumev2') def do_list(cs, args): """List all the volumes.""" @@ -158,6 +164,7 @@ def do_list(cs, args): 'all_tenants': all_tenants, 'name': args.name, 'status': args.status, + 'metadata': _extract_metadata(args) if args.metadata else None, } volumes = cs.volumes.list(search_opts=search_opts) _translate_volume_keys(volumes) From eb413f5373af70cbd41a0f9bf0800cf47d609dcc Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 12 Aug 2013 09:05:44 -0400 Subject: [PATCH 080/165] Fixing malformed assert message formatting This modification addresses Bug #1210296, specifically addressing malformed assert message formatting in assert_called_anytime. When verifying that an API method was called during test execution, the assert statement used will throw a TypeError when formatting the assert message, instead of the expected AssertionError. This occurs because a nested tuple in the format list is not properly expanded. The error itself likely exists because assert_called_anytime is not currently used in the python-cinderclient testing framework. The fix involves joining the arguments in the format list into a single tuple, allowing proper argument expansion. Fixes: bug 1210296 Change-Id: I6cf9dd55cff318e8a850637c540436c91dac08df --- cinderclient/tests/fakes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index 29260e1..41996a9 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -67,7 +67,7 @@ class FakeClient(object): break assert found, 'Expected %s %s; got %s' % ( - expected, self.client.callstack) + expected + (self.client.callstack, )) if body is not None: try: From 41cf3f193be7320f069963679889ef43662c490b Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 12 Aug 2013 08:55:22 -0400 Subject: [PATCH 081/165] Fixing erroneous clearing of test callstack This modification addresses Bug #1210292, specifically addressing the case where the test framework callstack is cleared when no asserts are raised in assert_called_anytime. This test method verifies that a specific API method call was made at any time during a test. If a test makes multiple calls to this method, and the first call yields no assertions, the subsequent calls will fail because the first assert in the method verifies that the callstack is non-empty. The error itself likely exists because assert_called_anytime is not currently used in the python-cinderclient testing framework. The fix involves deleting the last line in the method that clears the callstack. Fixes: bug 1210292 Change-Id: I8b7b740957841a328b2f0ca888190f758cbdd234 --- cinderclient/tests/fakes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index 29260e1..0216cac 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -78,8 +78,6 @@ class FakeClient(object): print(body) raise - self.client.callstack = [] - def clear_callstack(self): self.client.callstack = [] From 60543185e40afd50218fdb1c06cf1e3eaa746903 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Tue, 13 Aug 2013 22:36:13 +0200 Subject: [PATCH 082/165] Now building 1.0.5 --- debian/changelog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 91d2d91..4b59dd9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,6 @@ -python-cinderclient (1:1.0.4-3) UNRELEASED; urgency=low +python-cinderclient (1:1.0.5-1) unstable; urgency=low + * New upstream release. * Ran wrap-and-sort. -- Thomas Goirand Thu, 30 May 2013 13:42:32 +0800 From b328e84c6ca00fc43e426e2f1dbf934dc0eaba7a Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Tue, 13 Aug 2013 22:38:14 +0200 Subject: [PATCH 083/165] Now build-depends python-pbr (>= 0.5.20), (build-)depends: python-six, and using testrepository --- debian/changelog | 3 +++ debian/control | 6 +++++- debian/rules | 4 +--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 4b59dd9..fd81a13 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,9 @@ python-cinderclient (1:1.0.5-1) unstable; urgency=low * New upstream release. * Ran wrap-and-sort. + * Now build-depends python-pbr (>= 0.5.20), testrepository. + * Unit tests are now using testrepository. + * Added (build-)depends: python-six. -- Thomas Goirand Thu, 30 May 2013 13:42:32 +0800 diff --git a/debian/control b/debian/control index c9a8a72..60b5993 100644 --- a/debian/control +++ b/debian/control @@ -12,13 +12,16 @@ Build-Depends-Indep: openstack-pkg-tools, python-httplib2, python-mock, python-nose, + python-pbr (>= 0.5.20), python-prettytable, python-setuptools, python-setuptools-git, python-simplejson, + python-six, python-sphinx, python-subunit, - python-unittest2 + python-unittest2, + testrepository (>= 0.0.17) Standards-Version: 3.9.3 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary Vcs-Git: git://anonscm.debian.org/openstack/python-cinderclient.git @@ -30,6 +33,7 @@ Pre-Depends: dpkg (>= 1.15.6~) Depends: python-httplib2, python-prettytable, python-simplejson, + python-six, ${misc:Depends}, ${python:Depends} Provides: ${python:Provides} diff --git a/debian/rules b/debian/rules index a5e5ece..9c1bdef 100755 --- a/debian/rules +++ b/debian/rules @@ -27,7 +27,5 @@ override_dh_install: ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) override_dh_auto_test: - for i in `pyversions -s` ; do \ - $$i -m subunit.run discover -t ./ ./tests ; \ - done + python setup.py testr endif From 7b08b98a3bec2fbb9e7ee503f560880802fba412 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 6 Aug 2013 17:43:46 +0000 Subject: [PATCH 084/165] Sync strutils from oslo Sync strutils from oslo. Also import apiclient, gettextutils, and importutils. Change-Id: I565fd2cf40f2ea21842c6dbd581430b25d99fea6 Signed-off-by: Chuck Short --- .../openstack/common/apiclient/__init__.py | 16 + .../openstack/common/apiclient/auth.py | 227 ++++++++ .../openstack/common/apiclient/base.py | 492 ++++++++++++++++++ .../openstack/common/apiclient/client.py | 360 +++++++++++++ .../openstack/common/apiclient/exceptions.py | 446 ++++++++++++++++ .../openstack/common/apiclient/fake_client.py | 172 ++++++ cinderclient/openstack/common/gettextutils.py | 305 +++++++++++ cinderclient/openstack/common/importutils.py | 68 +++ cinderclient/openstack/common/strutils.py | 153 ++++-- tools/install_venv_common.py | 5 +- 10 files changed, 2208 insertions(+), 36 deletions(-) create mode 100644 cinderclient/openstack/common/apiclient/__init__.py create mode 100644 cinderclient/openstack/common/apiclient/auth.py create mode 100644 cinderclient/openstack/common/apiclient/base.py create mode 100644 cinderclient/openstack/common/apiclient/client.py create mode 100644 cinderclient/openstack/common/apiclient/exceptions.py create mode 100644 cinderclient/openstack/common/apiclient/fake_client.py create mode 100644 cinderclient/openstack/common/gettextutils.py create mode 100644 cinderclient/openstack/common/importutils.py 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/auth.py b/cinderclient/openstack/common/apiclient/auth.py new file mode 100644 index 0000000..374d20b --- /dev/null +++ b/cinderclient/openstack/common/apiclient/auth.py @@ -0,0 +1,227 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Spanish National Research Council. +# 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. + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import abc +import argparse +import logging +import os + +from stevedore import extension + +from cinderclient.openstack.common.apiclient import exceptions + + +logger = logging.getLogger(__name__) + + +_discovered_plugins = {} + + +def discover_auth_systems(): + """Discover the available auth-systems. + + This won't take into account the old style auth-systems. + """ + global _discovered_plugins + _discovered_plugins = {} + + def add_plugin(ext): + _discovered_plugins[ext.name] = ext.plugin + + ep_namespace = "cinderclient.openstack.common.apiclient.auth" + mgr = extension.ExtensionManager(ep_namespace) + mgr.map(add_plugin) + + +def load_auth_system_opts(parser): + """Load options needed by the available auth-systems into a parser. + + This function will try to populate the parser with options from the + available plugins. + """ + group = parser.add_argument_group("Common auth options") + BaseAuthPlugin.add_common_opts(group) + for name, auth_plugin in _discovered_plugins.iteritems(): + group = parser.add_argument_group( + "Auth-system '%s' options" % name, + conflict_handler="resolve") + auth_plugin.add_opts(group) + + +def load_plugin(auth_system): + try: + plugin_class = _discovered_plugins[auth_system] + except KeyError: + raise exceptions.AuthSystemNotFound(auth_system) + return plugin_class(auth_system=auth_system) + + +def load_plugin_from_args(args): + """Load requred plugin and populate it with options. + + Try to guess auth system if it is not specified. Systems are tried in + alphabetical order. + + :type args: argparse.Namespace + :raises: AuthorizationFailure + """ + auth_system = args.os_auth_system + if auth_system: + plugin = load_plugin(auth_system) + plugin.parse_opts(args) + plugin.sufficient_options() + return plugin + + for plugin_auth_system in sorted(_discovered_plugins.iterkeys()): + plugin_class = _discovered_plugins[plugin_auth_system] + plugin = plugin_class() + plugin.parse_opts(args) + try: + plugin.sufficient_options() + except exceptions.AuthPluginOptionsMissing: + continue + return plugin + raise exceptions.AuthPluginOptionsMissing(["auth_system"]) + + +class BaseAuthPlugin(object): + """Base class for authentication plugins. + + An authentication plugin needs to override at least the authenticate + method to be a valid plugin. + """ + + __metaclass__ = abc.ABCMeta + + auth_system = None + opt_names = [] + common_opt_names = [ + "auth_system", + "username", + "password", + "tenant_name", + "token", + "auth_url", + ] + + def __init__(self, auth_system=None, **kwargs): + self.auth_system = auth_system or self.auth_system + self.opts = dict((name, kwargs.get(name)) + for name in self.opt_names) + + @staticmethod + def _parser_add_opt(parser, opt): + """Add an option to parser in two variants. + + :param opt: option name (with underscores) + """ + dashed_opt = opt.replace("_", "-") + env_var = "OS_%s" % opt.upper() + arg_default = os.environ.get(env_var, "") + arg_help = "Defaults to env[%s]." % env_var + parser.add_argument( + "--os-%s" % dashed_opt, + metavar="<%s>" % dashed_opt, + default=arg_default, + help=arg_help) + parser.add_argument( + "--os_%s" % opt, + metavar="<%s>" % dashed_opt, + help=argparse.SUPPRESS) + + @classmethod + def add_opts(cls, parser): + """Populate the parser with the options for this plugin. + """ + for opt in cls.opt_names: + # use `BaseAuthPlugin.common_opt_names` since it is never + # changed in child classes + if opt not in BaseAuthPlugin.common_opt_names: + cls._parser_add_opt(parser, opt) + + @classmethod + def add_common_opts(cls, parser): + """Add options that are common for several plugins. + """ + for opt in cls.common_opt_names: + cls._parser_add_opt(parser, opt) + + @staticmethod + def get_opt(opt_name, args): + """Return option name and value. + + :param opt_name: name of the option, e.g., "username" + :param args: parsed arguments + """ + return (opt_name, getattr(args, "os_%s" % opt_name, None)) + + def parse_opts(self, args): + """Parse the actual auth-system options if any. + + This method is expected to populate the attribute `self.opts` with a + dict containing the options and values needed to make authentication. + """ + self.opts.update(dict(self.get_opt(opt_name, args) + for opt_name in self.opt_names)) + + def authenticate(self, http_client): + """Authenticate using plugin defined method. + + The method usually analyses `self.opts` and performs + a request to authentication server. + + :param http_client: client object that needs authentication + :type http_client: HTTPClient + :raises: AuthorizationFailure + """ + self.sufficient_options() + self._do_authenticate(http_client) + + @abc.abstractmethod + def _do_authenticate(self, http_client): + """Protected method for authentication. + """ + + def sufficient_options(self): + """Check if all required options are present. + + :raises: AuthPluginOptionsMissing + """ + missing = [opt + for opt in self.opt_names + if not self.opts.get(opt)] + if missing: + raise exceptions.AuthPluginOptionsMissing(missing) + + @abc.abstractmethod + def token_and_endpoint(self, endpoint_type, service_type): + """Return token and endpoint. + + :param service_type: Service type of the endpoint + :type service_type: string + :param endpoint_type: Type of endpoint. + Possible values: public or publicURL, + internal or internalURL, + admin or adminURL + :type endpoint_type: string + :returns: tuple of token and endpoint strings + :raises: EndpointException + """ diff --git a/cinderclient/openstack/common/apiclient/base.py b/cinderclient/openstack/common/apiclient/base.py new file mode 100644 index 0000000..1b3e790 --- /dev/null +++ b/cinderclient/openstack/common/apiclient/base.py @@ -0,0 +1,492 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack LLC +# Copyright 2012 Grid Dynamics +# 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. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +# E1102: %s is not callable +# pylint: disable=E1102 + +import abc +import urllib + +from cinderclient.openstack.common.apiclient import exceptions +from cinderclient.openstack.common import strutils + + +def getid(obj): + """Return id if argument is a Resource. + + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +# TODO(aababilov): call run_hooks() in HookableMixin's child classes +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + """Add a new hook of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param hook_func: hook function + """ + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + """Run all hooks of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param **args: args to be passed to every hook function + :param **kwargs: kwargs to be passed to every hook function + """ + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +class BaseManager(HookableMixin): + """Basic manager type providing common operations. + + Managers interact with a particular type of API (servers, flavors, images, + etc.) and provide CRUD operations for them. + """ + resource_class = None + + def __init__(self, client): + """Initializes BaseManager with `client`. + + :param client: instance of BaseClient descendant for HTTP requests + """ + super(BaseManager, self).__init__() + self.client = client + + def _list(self, url, response_key, obj_class=None, json=None): + """List the collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param obj_class: class for constructing the returned objects + (self.resource_class will be used by default) + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + """ + if json: + body = self.client.post(url, json=json).json() + else: + body = self.client.get(url).json() + + if obj_class is None: + obj_class = self.resource_class + + data = body[response_key] + # NOTE(ja): keystone returns values as list as {'values': [ ... ]} + # unlike other services which just return the list... + try: + data = data['values'] + except (KeyError, TypeError): + pass + + return [obj_class(self, res, loaded=True) for res in data if res] + + def _get(self, url, response_key): + """Get an object from collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'server' + """ + body = self.client.get(url).json() + return self.resource_class(self, body[response_key], loaded=True) + + def _head(self, url): + """Retrieve request headers for an object. + + :param url: a partial URL, e.g., '/servers' + """ + resp = self.client.head(url) + return resp.status_code == 204 + + def _post(self, url, json, response_key, return_raw=False): + """Create an object. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param return_raw: flag to force returning raw JSON instead of + Python object of self.resource_class + """ + body = self.client.post(url, json=json).json() + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + def _put(self, url, json=None, response_key=None): + """Update an object with PUT method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + resp = self.client.put(url, json=json) + # PUT requests may not return a body + if resp.content: + body = resp.json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _patch(self, url, json=None, response_key=None): + """Update an object with PATCH method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + body = self.client.patch(url, json=json).json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _delete(self, url): + """Delete an object. + + :param url: a partial URL, e.g., '/servers/my-server' + """ + return self.client.delete(url) + + +class ManagerWithFind(BaseManager): + """Manager 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``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + matches = self.findall(**kwargs) + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(msg) + elif num_matches > 1: + raise exceptions.NoUniqueMatch() + else: + return matches[0] + + def findall(self, **kwargs): + """Find all items with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + found = [] + searches = kwargs.items() + + for obj in self.list(): + try: + if all(getattr(obj, attr) == value + for (attr, value) in searches): + found.append(obj) + except AttributeError: + continue + + return found + + +class CrudManager(BaseManager): + """Base manager class for manipulating entities. + + Children of this class are expected to define a `collection_key` and `key`. + + - `collection_key`: Usually a plural noun by convention (e.g. `entities`); + used to refer collections in both URL's (e.g. `/v3/entities`) and JSON + objects containing a list of member resources (e.g. `{'entities': [{}, + {}, {}]}`). + - `key`: Usually a singular noun by convention (e.g. `entity`); used to + refer to an individual member of the collection. + + """ + collection_key = None + key = None + + def build_url(self, base_url=None, **kwargs): + """Builds a resource URL for the given kwargs. + + Given an example collection where `collection_key = 'entities'` and + `key = 'entity'`, the following URL's could be generated. + + By default, the URL will represent a collection of entities, e.g.:: + + /entities + + If kwargs contains an `entity_id`, then the URL will represent a + specific member, e.g.:: + + /entities/{entity_id} + + :param base_url: if provided, the generated URL will be appended to it + """ + url = base_url if base_url is not None else '' + + url += '/%s' % self.collection_key + + # do we have a specific entity? + entity_id = kwargs.get('%s_id' % self.key) + if entity_id is not None: + url += '/%s' % entity_id + + return url + + def _filter_kwargs(self, kwargs): + """Drop null values and handle ids.""" + for key, ref in kwargs.copy().iteritems(): + if ref is None: + kwargs.pop(key) + else: + if isinstance(ref, Resource): + kwargs.pop(key) + kwargs['%s_id' % key] = getid(ref) + return kwargs + + def create(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._post( + self.build_url(**kwargs), + {self.key: kwargs}, + self.key) + + def get(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._get( + self.build_url(**kwargs), + self.key) + + def head(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._head(self.build_url(**kwargs)) + + def list(self, base_url=None, **kwargs): + """List the collection. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + + def put(self, base_url=None, **kwargs): + """Update an element. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._put(self.build_url(base_url=base_url, **kwargs)) + + def update(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + params = kwargs.copy() + params.pop('%s_id' % self.key) + + return self._patch( + self.build_url(**kwargs), + {self.key: params}, + self.key) + + def delete(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._delete( + self.build_url(**kwargs)) + + def find(self, base_url=None, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + rl = self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + num = len(rl) + + if num == 0: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(404, msg) + elif num > 1: + raise exceptions.NoUniqueMatch + else: + return rl[0] + + +class Extension(HookableMixin): + """Extension descriptor.""" + + SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') + manager_class = None + + def __init__(self, name, module): + super(Extension, self).__init__() + self.name = name + self.module = module + self._parse_extension_module() + + def _parse_extension_module(self): + self.manager_class = None + for attr_name, attr_value in self.module.__dict__.items(): + if attr_name in self.SUPPORTED_HOOKS: + self.add_hook(attr_name, attr_value) + else: + try: + if issubclass(attr_value, BaseManager): + self.manager_class = attr_value + except TypeError: + pass + + def __repr__(self): + return "" % self.name + + +class Resource(object): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. + """ + + HUMAN_ID = False + NAME_ATTR = 'name' + + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def __repr__(self): + reprkeys = sorted(k + for k in 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) + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion. + """ + if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: + return strutils.to_slug(getattr(self, self.NAME_ATTR)) + return None + + def _add_details(self, info): + for (k, v) in info.iteritems(): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + #NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val diff --git a/cinderclient/openstack/common/apiclient/client.py b/cinderclient/openstack/common/apiclient/client.py new file mode 100644 index 0000000..85837da --- /dev/null +++ b/cinderclient/openstack/common/apiclient/client.py @@ -0,0 +1,360 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack LLC +# Copyright 2011 Piston Cloud Computing, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 Grid Dynamics +# 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. + +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import logging +import time + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from cinderclient.openstack.common.apiclient import exceptions +from cinderclient.openstack.common import importutils + + +_logger = logging.getLogger(__name__) + + +class HTTPClient(object): + """This client handles sending HTTP requests to OpenStack servers. + + Features: + - share authentication information between several clients to different + services (e.g., for compute and image clients); + - reissue authentication request for expired tokens; + - encode/decode JSON bodies; + - raise exeptions on HTTP errors; + - pluggable authentication; + - store authentication information in a keyring; + - store time spent for requests; + - register clients for particular services, so one can use + `http_client.identity` or `http_client.compute`; + - log requests and responses in a format that is easy to copy-and-paste + into terminal and send the same request with curl. + """ + + user_agent = "cinderclient.openstack.common.apiclient" + + def __init__(self, + auth_plugin, + region_name=None, + endpoint_type="publicURL", + original_ip=None, + verify=True, + cert=None, + timeout=None, + timings=False, + keyring_saver=None, + debug=False, + user_agent=None, + http=None): + self.auth_plugin = auth_plugin + + self.endpoint_type = endpoint_type + self.region_name = region_name + + self.original_ip = original_ip + self.timeout = timeout + self.verify = verify + self.cert = cert + + self.keyring_saver = keyring_saver + self.debug = debug + self.user_agent = user_agent or self.user_agent + + self.times = [] # [("item", starttime, endtime), ...] + self.timings = timings + + # requests within the same session can reuse TCP connections from pool + self.http = http or requests.Session() + + self.cached_token = None + + def _http_log_req(self, method, url, kwargs): + if not self.debug: + return + + string_parts = [ + "curl -i", + "-X '%s'" % method, + "'%s'" % url, + ] + + for element in kwargs['headers']: + header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + string_parts.append(header) + + _logger.debug("REQ: %s" % " ".join(string_parts)) + if 'data' in kwargs: + _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) + + def _http_log_resp(self, resp): + if not self.debug: + return + _logger.debug( + "RESP: [%s] %s\n", + resp.status_code, + resp.headers) + if resp._content_consumed: + _logger.debug( + "RESP BODY: %s\n", + resp.text) + + def serialize(self, kwargs): + if kwargs.get('json') is not None: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json.dumps(kwargs['json']) + try: + del kwargs['json'] + except KeyError: + pass + + def get_timings(self): + return self.times + + def reset_timings(self): + self.times = [] + + def request(self, method, url, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around `requests.Session.request` to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to +' requests.Session.request (such as `headers`) or `json` + that will be encoded as JSON and used as `data` argument + """ + kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs["headers"]["User-Agent"] = self.user_agent + if self.original_ip: + kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( + self.original_ip, self.user_agent) + if self.timeout is not None: + kwargs.setdefault("timeout", self.timeout) + kwargs.setdefault("verify", self.verify) + if self.cert is not None: + kwargs.setdefault("cert", self.cert) + self.serialize(kwargs) + + self._http_log_req(method, url, kwargs) + if self.timings: + start_time = time.time() + resp = self.http.request(method, url, **kwargs) + if self.timings: + self.times.append(("%s %s" % (method, url), + start_time, time.time())) + self._http_log_resp(resp) + + if resp.status_code >= 400: + _logger.debug( + "Request returned failure status: %s", + resp.status_code) + raise exceptions.from_response(resp, method, url) + + return resp + + @staticmethod + def concat_url(endpoint, url): + """Concatenate endpoint and final URL. + + E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to + "http://keystone/v2.0/tokens". + + :param endpoint: the base URL + :param url: the final URL + """ + return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) + + def client_request(self, client, method, url, **kwargs): + """Send an http request using `client`'s endpoint and specified `url`. + + If request was rejected as unauthorized (possibly because the token is + expired), issue one authorization attempt and send the request once + again. + + :param client: instance of BaseClient descendant + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to +' `HTTPClient.request` + """ + + filter_args = { + "endpoint_type": client.endpoint_type or self.endpoint_type, + "service_type": client.service_type, + } + token, endpoint = (self.cached_token, client.cached_endpoint) + just_authenticated = False + if not (token and endpoint): + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + pass + if not (token and endpoint): + self.authenticate() + just_authenticated = True + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + if not (token and endpoint): + raise exceptions.AuthorizationFailure( + "Cannot find endpoint or token for request") + + old_token_endpoint = (token, endpoint) + kwargs.setdefault("headers", {})["X-Auth-Token"] = token + self.cached_token = token + client.cached_endpoint = endpoint + # Perform the request once. If we get Unauthorized, then it + # might be because the auth token expired, so try to + # re-authenticate and try again. If it still fails, bail. + try: + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + except exceptions.Unauthorized as unauth_ex: + if just_authenticated: + raise + self.cached_token = None + client.cached_endpoint = None + self.authenticate() + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + raise unauth_ex + if (not (token and endpoint) or + old_token_endpoint == (token, endpoint)): + raise unauth_ex + self.cached_token = token + client.cached_endpoint = endpoint + kwargs["headers"]["X-Auth-Token"] = token + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + + def add_client(self, base_client_instance): + """Add a new instance of :class:`BaseClient` descendant. + + `self` will store a reference to `base_client_instance`. + + Example: + + >>> def test_clients(): + ... from keystoneclient.auth import keystone + ... from openstack.common.apiclient import client + ... auth = keystone.KeystoneAuthPlugin( + ... username="user", password="pass", tenant_name="tenant", + ... auth_url="http://auth:5000/v2.0") + ... openstack_client = client.HTTPClient(auth) + ... # create nova client + ... from novaclient.v1_1 import client + ... client.Client(openstack_client) + ... # create keystone client + ... from keystoneclient.v2_0 import client + ... client.Client(openstack_client) + ... # use them + ... openstack_client.identity.tenants.list() + ... openstack_client.compute.servers.list() + """ + service_type = base_client_instance.service_type + if service_type and not hasattr(self, service_type): + setattr(self, service_type, base_client_instance) + + def authenticate(self): + self.auth_plugin.authenticate(self) + # Store the authentication results in the keyring for later requests + if self.keyring_saver: + self.keyring_saver.save(self) + + +class BaseClient(object): + """Top-level object to access the OpenStack API. + + This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` + will handle a bunch of issues such as authentication. + """ + + service_type = None + endpoint_type = None # "publicURL" will be used + cached_endpoint = None + + def __init__(self, http_client, extensions=None): + self.http_client = http_client + http_client.add_client(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + def client_request(self, method, url, **kwargs): + return self.http_client.client_request( + self, method, url, **kwargs) + + def head(self, url, **kwargs): + return self.client_request("HEAD", url, **kwargs) + + def get(self, url, **kwargs): + return self.client_request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.client_request("POST", url, **kwargs) + + def put(self, url, **kwargs): + return self.client_request("PUT", url, **kwargs) + + def delete(self, url, **kwargs): + return self.client_request("DELETE", url, **kwargs) + + def patch(self, url, **kwargs): + return self.client_request("PATCH", url, **kwargs) + + @staticmethod + def get_class(api_name, version, version_map): + """Returns the client class for the requested API version + + :param api_name: the name of the API, e.g. 'compute', 'image', etc + :param version: the requested API version + :param version_map: a dict of client classes keyed by version + :rtype: a client class for the requested API version + """ + try: + client_path = version_map[str(version)] + except (KeyError, ValueError): + msg = "Invalid %s client version '%s'. must be one of: %s" % ( + (api_name, version, ', '.join(version_map.keys()))) + raise exceptions.UnsupportedVersion(msg) + + return importutils.import_class(client_path) diff --git a/cinderclient/openstack/common/apiclient/exceptions.py b/cinderclient/openstack/common/apiclient/exceptions.py new file mode 100644 index 0000000..b03def7 --- /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/openstack/common/apiclient/fake_client.py b/cinderclient/openstack/common/apiclient/fake_client.py new file mode 100644 index 0000000..914cebd --- /dev/null +++ b/cinderclient/openstack/common/apiclient/fake_client.py @@ -0,0 +1,172 @@ +# 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. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +# W0102: Dangerous default value %s as argument +# pylint: disable=W0102 + +import json +import urlparse + +import requests + +from cinderclient.openstack.common.apiclient import client + + +def assert_has_keys(dct, required=[], optional=[]): + for k in required: + try: + assert k in dct + except AssertionError: + extra_keys = set(dct.keys()).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class TestResponse(requests.Response): + """Wrap requests.Response and provide a convenient initialization. + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._content_consumed = True + if isinstance(data, dict): + self.status_code = data.get('status_code', 200) + # Fake the text attribute to streamline Response creation + text = data.get('text', "") + if isinstance(text, (dict, list)): + self._content = json.dumps(text) + default_headers = { + "Content-Type": "application/json", + } + else: + self._content = text + default_headers = {} + self.headers = data.get('headers') or default_headers + else: + self.status_code = data + + def __eq__(self, other): + return (self.status_code == other.status_code and + self.headers == other.headers and + self._content == other._content) + + +class FakeHTTPClient(client.HTTPClient): + + def __init__(self, *args, **kwargs): + self.callstack = [] + self.fixtures = kwargs.pop("fixtures", None) or {} + if not args and not "auth_plugin" in kwargs: + args = (None, ) + super(FakeHTTPClient, self).__init__(*args, **kwargs) + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called. + """ + expected = (method, url) + called = self.callstack[pos][0:2] + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + assert expected == called, 'Expected %s %s; got %s %s' % \ + (expected + called) + + if body is not None: + if self.callstack[pos][3] != body: + raise AssertionError('%r != %r' % + (self.callstack[pos][3], body)) + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test. + """ + expected = (method, url) + + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + entry = None + for entry in self.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s %s; got %s' % \ + (method, url, self.callstack) + if body is not None: + assert entry[3] == body, "%s != %s" % (entry[3], body) + + self.callstack = [] + + def clear_callstack(self): + self.callstack = [] + + def authenticate(self): + pass + + def client_request(self, client, method, url, **kwargs): + # Check that certain things are called correctly + if method in ["GET", "DELETE"]: + assert "json" not in kwargs + + # Note the call + self.callstack.append( + (method, + url, + kwargs.get("headers") or {}, + kwargs.get("json") or kwargs.get("data"))) + try: + fixture = self.fixtures[url][method] + except KeyError: + pass + else: + return TestResponse({"headers": fixture[0], + "text": fixture[1]}) + + # Call the method + args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + resp = getattr(self, callback)(**kwargs) + if len(resp) == 3: + status, headers, body = resp + else: + status, body = resp + headers = {} + return TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) diff --git a/cinderclient/openstack/common/gettextutils.py b/cinderclient/openstack/common/gettextutils.py new file mode 100644 index 0000000..e887869 --- /dev/null +++ b/cinderclient/openstack/common/gettextutils.py @@ -0,0 +1,305 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat, Inc. +# 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. + +""" +gettext for openstack-common modules. + +Usual usage in an openstack.common module: + + from cinderclient.openstack.common.gettextutils import _ +""" + +import copy +import gettext +import logging.handlers +import os +import re +import UserString + +from babel import localedata +import six + +_localedir = os.environ.get('cinderclient'.upper() + '_LOCALEDIR') +_t = gettext.translation('cinderclient', localedir=_localedir, fallback=True) + +_AVAILABLE_LANGUAGES = [] + + +def _(msg): + return _t.ugettext(msg) + + +def install(domain, lazy=False): + """Install a _() function using the given translation domain. + + Given a translation domain, install a _() function using gettext's + install() function. + + The main difference from gettext.install() is that we allow + overriding the default localedir (e.g. /usr/share/locale) using + a translation-domain-specific environment variable (e.g. + NOVA_LOCALEDIR). + + :param domain: the translation domain + :param lazy: indicates whether or not to install the lazy _() function. + The lazy _() introduces a way to do deferred translation + of messages by installing a _ that builds Message objects, + instead of strings, which can then be lazily translated into + any available locale. + """ + if lazy: + # NOTE(mrodden): Lazy gettext functionality. + # + # The following introduces a deferred way to do translations on + # messages in OpenStack. We override the standard _() function + # and % (format string) operation to build Message objects that can + # later be translated when we have more information. + # + # Also included below is an example LocaleHandler that translates + # Messages to an associated locale, effectively allowing many logs, + # each with their own locale. + + def _lazy_gettext(msg): + """Create and return a Message object. + + Lazy gettext function for a given domain, it is a factory method + for a project/module to get a lazy gettext function for its own + translation domain (i.e. nova, glance, cinder, etc.) + + Message encapsulates a string so that we can translate + it later when needed. + """ + return Message(msg, domain) + + import __builtin__ + __builtin__.__dict__['_'] = _lazy_gettext + else: + localedir = '%s_LOCALEDIR' % domain.upper() + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) + + +class Message(UserString.UserString, object): + """Class used to encapsulate translatable messages.""" + def __init__(self, msg, domain): + # _msg is the gettext msgid and should never change + self._msg = msg + self._left_extra_msg = '' + self._right_extra_msg = '' + self.params = None + self.locale = None + self.domain = domain + + @property + def data(self): + # NOTE(mrodden): this should always resolve to a unicode string + # that best represents the state of the message currently + + localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') + if self.locale: + lang = gettext.translation(self.domain, + localedir=localedir, + languages=[self.locale], + fallback=True) + else: + # use system locale for translations + lang = gettext.translation(self.domain, + localedir=localedir, + fallback=True) + + full_msg = (self._left_extra_msg + + lang.ugettext(self._msg) + + self._right_extra_msg) + + if self.params is not None: + full_msg = full_msg % self.params + + return six.text_type(full_msg) + + def _save_dictionary_parameter(self, dict_param): + full_msg = self.data + # look for %(blah) fields in string; + # ignore %% and deal with the + # case where % is first character on the line + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) + + # if we don't find any %(blah) blocks but have a %s + if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): + # apparently the full dictionary is the parameter + params = copy.deepcopy(dict_param) + else: + params = {} + for key in keys: + try: + params[key] = copy.deepcopy(dict_param[key]) + except TypeError: + # cast uncopyable thing to unicode string + params[key] = unicode(dict_param[key]) + + return params + + def _save_parameters(self, other): + # we check for None later to see if + # we actually have parameters to inject, + # so encapsulate if our parameter is actually None + if other is None: + self.params = (other, ) + elif isinstance(other, dict): + self.params = self._save_dictionary_parameter(other) + else: + # fallback to casting to unicode, + # this will handle the problematic python code-like + # objects that cannot be deep-copied + try: + self.params = copy.deepcopy(other) + except TypeError: + self.params = unicode(other) + + return self + + # overrides to be more string-like + def __unicode__(self): + return self.data + + def __str__(self): + return self.data.encode('utf-8') + + def __getstate__(self): + to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', + 'domain', 'params', 'locale'] + new_dict = self.__dict__.fromkeys(to_copy) + for attr in to_copy: + new_dict[attr] = copy.deepcopy(self.__dict__[attr]) + + return new_dict + + def __setstate__(self, state): + for (k, v) in state.items(): + setattr(self, k, v) + + # operator overloads + def __add__(self, other): + copied = copy.deepcopy(self) + copied._right_extra_msg += other.__str__() + return copied + + def __radd__(self, other): + copied = copy.deepcopy(self) + copied._left_extra_msg += other.__str__() + return copied + + def __mod__(self, other): + # do a format string to catch and raise + # any possible KeyErrors from missing parameters + self.data % other + copied = copy.deepcopy(self) + return copied._save_parameters(other) + + def __mul__(self, other): + return self.data * other + + def __rmul__(self, other): + return other * self.data + + def __getitem__(self, key): + return self.data[key] + + def __getslice__(self, start, end): + return self.data.__getslice__(start, end) + + def __getattribute__(self, name): + # NOTE(mrodden): handle lossy operations that we can't deal with yet + # These override the UserString implementation, since UserString + # uses our __class__ attribute to try and build a new message + # after running the inner data string through the operation. + # At that point, we have lost the gettext message id and can just + # safely resolve to a string instead. + ops = ['capitalize', 'center', 'decode', 'encode', + 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', + 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] + if name in ops: + return getattr(self.data, name) + else: + return UserString.UserString.__getattribute__(self, name) + + +def get_available_languages(domain): + """Lists the available languages for the given translation domain. + + :param domain: the domain to get languages for + """ + if _AVAILABLE_LANGUAGES: + return _AVAILABLE_LANGUAGES + + localedir = '%s_LOCALEDIR' % domain.upper() + find = lambda x: gettext.find(domain, + localedir=os.environ.get(localedir), + languages=[x]) + + # NOTE(mrodden): en_US should always be available (and first in case + # order matters) since our in-line message strings are en_US + _AVAILABLE_LANGUAGES.append('en_US') + # NOTE(luisg): Babel <1.0 used a function called list(), which was + # renamed to locale_identifiers() in >=1.0, the requirements master list + # requires >=0.9.6, uncapped, so defensively work with both. We can remove + # this check when the master list updates to >=1.0, and all projects udpate + list_identifiers = (getattr(localedata, 'list', None) or + getattr(localedata, 'locale_identifiers')) + locale_identifiers = list_identifiers() + for i in locale_identifiers: + if find(i) is not None: + _AVAILABLE_LANGUAGES.append(i) + return _AVAILABLE_LANGUAGES + + +def get_localized_message(message, user_locale): + """Gets a localized version of the given message in the given locale.""" + if (isinstance(message, Message)): + if user_locale: + message.locale = user_locale + return unicode(message) + else: + return message + + +class LocaleHandler(logging.Handler): + """Handler that can have a locale associated to translate Messages. + + A quick example of how to utilize the Message class above. + LocaleHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating the internal Message. + """ + + def __init__(self, locale, target): + """Initialize a LocaleHandler + + :param locale: locale to use for translating messages + :param target: logging.Handler object to forward + LogRecord objects to after translation + """ + logging.Handler.__init__(self) + self.locale = locale + self.target = target + + def emit(self, record): + if isinstance(record.msg, Message): + # set the locale and resolve to a string + record.msg.locale = self.locale + + self.target.emit(record) diff --git a/cinderclient/openstack/common/importutils.py b/cinderclient/openstack/common/importutils.py new file mode 100644 index 0000000..7a303f9 --- /dev/null +++ b/cinderclient/openstack/common/importutils.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 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. + +""" +Import related utilities and helper functions. +""" + +import sys +import traceback + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + try: + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + except (ValueError, AttributeError): + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) + + +def import_object(import_str, *args, **kwargs): + """Import a class and return an instance of it.""" + return import_class(import_str)(*args, **kwargs) + + +def import_object_ns(name_space, import_str, *args, **kwargs): + """Tries to import object from default namespace. + + Imports a class and return an instance of it, first by trying + to find the class in a default namespace, then failing back to + a full path if not found in the default namespace. + """ + import_value = "%s.%s" % (name_space, import_str) + try: + return import_class(import_value)(*args, **kwargs) + except ImportError: + return import_class(import_str)(*args, **kwargs) + + +def import_module(import_str): + """Import a module.""" + __import__(import_str) + return sys.modules[import_str] + + +def try_import(import_str, default=None): + """Try to import a module and if it fails return default.""" + try: + return import_module(import_str) + except ImportError: + return default diff --git a/cinderclient/openstack/common/strutils.py b/cinderclient/openstack/common/strutils.py index 7813b64..7c9fcec 100644 --- a/cinderclient/openstack/common/strutils.py +++ b/cinderclient/openstack/common/strutils.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,15 +19,35 @@ System-level utilities and helper functions. """ -import logging +import re import sys +import unicodedata -LOG = logging.getLogger(__name__) +import six + +from cinderclient.openstack.common.gettextutils import _ # noqa + + +# Used for looking up extensions of text +# to their 'multiplied' byte amount +BYTE_MULTIPLIERS = { + '': 1, + 't': 1024 ** 4, + 'g': 1024 ** 3, + 'm': 1024 ** 2, + 'k': 1024, +} +BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') + +TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') +FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') + +SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") +SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") def int_from_bool_as_string(subject): - """ - Interpret a string as a boolean and return either 1 or 0. + """Interpret a string as a boolean and return either 1 or 0. Any string value in: @@ -40,42 +60,53 @@ def int_from_bool_as_string(subject): return bool_from_string(subject) and 1 or 0 -def bool_from_string(subject): +def bool_from_string(subject, strict=False): + """Interpret a string as a boolean. + + A case-insensitive match is performed such that strings matching 't', + 'true', 'on', 'y', 'yes', or '1' are considered True and, when + `strict=False`, anything else is considered False. + + Useful for JSON-decoded stuff and config file parsing. + + If `strict=True`, unrecognized values, including None, will raise a + ValueError which is useful when parsing values passed in from an API call. + Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ - Interpret a string as a boolean. + if not isinstance(subject, six.string_types): + subject = str(subject) - Any string value in: + lowered = subject.strip().lower() - ('True', 'true', 'On', 'on', 'Yes', 'yes', '1') - - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - if isinstance(subject, bool): - return subject - if isinstance(subject, basestring): - if subject.strip().lower() in ('true', 'on', 'yes', '1'): - return True - return False + if lowered in TRUE_STRINGS: + return True + elif lowered in FALSE_STRINGS: + return False + elif strict: + acceptable = ', '.join( + "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) + msg = _("Unrecognized value '%(val)s', acceptable values are:" + " %(acceptable)s") % {'val': subject, + 'acceptable': acceptable} + raise ValueError(msg) + else: + return False def safe_decode(text, incoming=None, errors='strict'): - """ - Decodes incoming str using `incoming` if they're - not already unicode. + """Decodes incoming str using `incoming` if they're not already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. - :raises TypeError: If text is not an isntance of basestring + :raises TypeError: If text is not an isntance of str """ - if not isinstance(text, basestring): + if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) - if isinstance(text, unicode): + if isinstance(text, six.text_type): return text if not incoming: @@ -102,11 +133,10 @@ def safe_decode(text, incoming=None, errors='strict'): def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): - """ - Encodes incoming str/unicode using `encoding`. If - incoming is not specified, text is expected to - be encoded with current python's default encoding. - (`sys.getdefaultencoding`) + """Encodes incoming str/unicode using `encoding`. + + If incoming is not specified, text is expected to be encoded with + current python's default encoding. (`sys.getdefaultencoding`) :param incoming: Text's current encoding :param encoding: Expected encoding for text (Default UTF-8) @@ -114,16 +144,16 @@ def safe_encode(text, incoming=None, values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. - :raises TypeError: If text is not an isntance of basestring + :raises TypeError: If text is not an isntance of str """ - if not isinstance(text, basestring): + if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) if not incoming: incoming = (sys.stdin.encoding or sys.getdefaultencoding()) - if isinstance(text, unicode): + if isinstance(text, six.text_type): return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` @@ -131,3 +161,58 @@ def safe_encode(text, incoming=None, return text.encode(encoding, errors) return text + + +def to_bytes(text, default=0): + """Converts a string into an integer of bytes. + + Looks at the last characters of the text to determine + what conversion is needed to turn the input text into a byte number. + Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) + + :param text: String input for bytes size conversion. + :param default: Default return value when text is blank. + + """ + match = BYTE_REGEX.search(text) + if match: + magnitude = int(match.group(1)) + mult_key_org = match.group(2) + if not mult_key_org: + return magnitude + elif text: + msg = _('Invalid string format: %s') % text + raise TypeError(msg) + else: + return default + mult_key = mult_key_org.lower().replace('b', '', 1) + multiplier = BYTE_MULTIPLIERS.get(mult_key) + if multiplier is None: + msg = _('Unknown byte multiplier: %s') % mult_key_org + raise TypeError(msg) + return magnitude * multiplier + + +def to_slug(value, incoming=None, errors="strict"): + """Normalize string. + + Convert to lowercase, remove non-word characters, and convert spaces + to hyphens. + + Inspired by Django's `slugify` filter. + + :param value: Text to slugify + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: slugified unicode representation of `value` + :raises TypeError: If text is not an instance of str + """ + value = safe_decode(value, incoming, errors) + # NOTE(aababilov): no need to use safe_(encode|decode) here: + # encodings are always "ascii", error handling is always "ignore" + # and types are always known (first: unicode; second: str) + value = unicodedata.normalize("NFKD", value).encode( + "ascii", "ignore").decode("ascii") + value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() + return SLUGIFY_HYPHENATE_RE.sub("-", value) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index f428c1e..6ce5d00 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -114,9 +114,10 @@ class InstallVenv(object): 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') + # setuptools and pbr + self.pip_install('pip>=1.4') self.pip_install('setuptools') + self.pip_install('pbr') self.pip_install('-r', self.requirements) self.pip_install('-r', self.test_requirements) From 109415c26dca7e1f2fb302c25f0788037cc14023 Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Wed, 21 Aug 2013 14:20:41 -0400 Subject: [PATCH 085/165] Add volume encryption metadata to cinderclient This modification adds support for volume type encryption to python-cinderclient, supporting two Cinder API extensions. The first support set provides accessors to python-cinderclient for retrieving volume encryption metadata from Cinder. These changes provide other services (e.g., Nova) access to encryption metadata (e.g., encryption key UUIDs). See the volume encryption key API extension in Cinder for more information. The second support set creates a new python-cinderclient resource manager, along with matching shell commands, that provides creation and accessor operations for encryption type information. These operations allow users and services to define encryption information (e.g., cipher, key size, encryption provider) for a pre-existing volume type. See the volume type encryption API extension in Cinder for more information. blueprint encrypt-cinder-volumes Change-Id: Id4b2425d699678eb1997863362ddb9bf5ba6f033 --- cinderclient/tests/v1/fakes.py | 26 +++++ cinderclient/tests/v1/test_shell.py | 57 +++++++++++ .../tests/v1/test_volume_encryption_types.py | 95 ++++++++++++++++++ .../tests/v1/test_volume_transfers.py | 2 +- cinderclient/tests/v1/test_volumes.py | 4 + cinderclient/tests/v2/fakes.py | 26 +++++ cinderclient/tests/v2/test_shell.py | 57 +++++++++++ .../tests/v2/test_volume_encryption_types.py | 95 ++++++++++++++++++ .../tests/v2/test_volume_transfers.py | 2 +- cinderclient/tests/v2/test_volumes.py | 4 + cinderclient/v1/client.py | 3 + cinderclient/v1/shell.py | 85 +++++++++++++++- cinderclient/v1/volume_encryption_types.py | 96 +++++++++++++++++++ cinderclient/v1/volume_types.py | 2 +- cinderclient/v1/volumes.py | 11 ++- cinderclient/v2/client.py | 3 + cinderclient/v2/shell.py | 83 ++++++++++++++++ cinderclient/v2/volume_encryption_types.py | 96 +++++++++++++++++++ cinderclient/v2/volumes.py | 11 ++- 19 files changed, 752 insertions(+), 6 deletions(-) create mode 100644 cinderclient/tests/v1/test_volume_encryption_types.py create mode 100644 cinderclient/tests/v2/test_volume_encryption_types.py create mode 100644 cinderclient/v1/volume_encryption_types.py create mode 100644 cinderclient/v2/volume_encryption_types.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index c83f5ad..88e58ea 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -276,6 +276,10 @@ class FakeHTTPClient(base_client.HTTPClient): r = {'volume': self.get_volumes_detail()[2]['volumes'][0]} return (200, {}, r) + def get_volumes_1234_encryption(self, **kw): + r = {'encryption_key_id': 'id'} + return (200, {}, r) + def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 @@ -383,6 +387,11 @@ class FakeHTTPClient(base_client.HTTPClient): 'name': 'test-type-1', 'extra_specs': {}}}) + def get_types_2(self, **kw): + return (200, {}, {'volume_type': {'id': 2, + 'name': 'test-type-2', + 'extra_specs': {}}}) + def post_types(self, body, **kw): return (202, {}, {'volume_type': {'id': 3, 'name': 'test-type-3', @@ -398,6 +407,23 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_types_1(self, **kw): return (202, {}, None) + # + # VolumeEncryptionTypes + # + def get_types_1_encryption(self, **kw): + return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test', + 'cipher': 'test', 'key_size': 1, + 'control_location': 'front'}) + + def get_types_2_encryption(self, **kw): + return (200, {}, {}) + + def post_types_2_encryption(self, body, **kw): + return (200, {}, {'encryption': {}}) + + def put_types_1_encryption_1(self, body, **kw): + return (200, {}, {}) + # # Set/Unset metadata # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 71d7896..014df94 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -203,3 +203,60 @@ class ShellTest(utils.TestCase): self.run_command('snapshot-reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + + def test_encryption_type_list(self): + """ + Test encryption-type-list shell command. + + Verify a series of GET requests are made: + - one to get the volume type list information + - one per volume type to retrieve the encryption type information + """ + self.run_command('encryption-type-list') + self.assert_called_anytime('GET', '/types') + self.assert_called_anytime('GET', '/types/1/encryption') + self.assert_called_anytime('GET', '/types/2/encryption') + + def test_encryption_type_show(self): + """ + Test encryption-type-show shell command. + + Verify two GET requests are made per command invocation: + - one to get the volume type information + - one to get the encryption type information + """ + self.run_command('encryption-type-show 1') + self.assert_called('GET', '/types/1/encryption') + self.assert_called_anytime('GET', '/types/1') + + def test_encryption_type_create(self): + """ + Test encryption-type-create shell command. + + Verify GET and POST requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one POST request to create the new encryption type + """ + expected = {'encryption': {'cipher': None, 'key_size': None, + 'provider': 'TestProvider', + 'control_location': None}} + self.run_command('encryption-type-create 2 TestProvider') + self.assert_called('POST', '/types/2/encryption', body=expected) + self.assert_called_anytime('GET', '/types/2') + + def test_encryption_type_update(self): + """ + Test encryption-type-update shell command. + + Verify two GETs/one PUT requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one GET request to retrieve the relevant encryption type information + - one PUT request to update the encryption type information + """ + self.skipTest("Not implemented") + + def test_encryption_type_delete(self): + """ + Test encryption-type-delete shell command. + """ + self.skipTest("Not implemented") diff --git a/cinderclient/tests/v1/test_volume_encryption_types.py b/cinderclient/tests/v1/test_volume_encryption_types.py new file mode 100644 index 0000000..d9af7d8 --- /dev/null +++ b/cinderclient/tests/v1/test_volume_encryption_types.py @@ -0,0 +1,95 @@ +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# 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.v1.volume_encryption_types import VolumeEncryptionType +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + +cs = fakes.FakeClient() + + +class VolumeEncryptionTypesTest(utils.TestCase): + """ + Test suite for the Volume Encryption Types Resource and Manager. + """ + + def test_list(self): + """ + Unit test for VolumeEncryptionTypesManager.list + + Verify that a series of GET requests are made: + - one GET request for the list of volume types + - one GET request per volume type for encryption type information + + Verify that all returned information is :class: VolumeEncryptionType + """ + encryption_types = cs.volume_encryption_types.list() + cs.assert_called_anytime('GET', '/types') + cs.assert_called_anytime('GET', '/types/2/encryption') + cs.assert_called_anytime('GET', '/types/1/encryption') + for encryption_type in encryption_types: + self.assertIsInstance(encryption_type, VolumeEncryptionType) + + def test_get(self): + """ + Unit test for VolumeEncryptionTypesManager.get + + Verify that one GET request is made for the volume type encryption + type information. Verify that returned information is :class: + VolumeEncryptionType + """ + encryption_type = cs.volume_encryption_types.get(1) + cs.assert_called('GET', '/types/1/encryption') + self.assertIsInstance(encryption_type, VolumeEncryptionType) + + def test_get_no_encryption(self): + """ + Unit test for VolumeEncryptionTypesManager.get + + Verify that a request on a volume type with no associated encryption + type information returns a VolumeEncryptionType with no attributes. + """ + encryption_type = cs.volume_encryption_types.get(2) + self.assertIsInstance(encryption_type, VolumeEncryptionType) + self.assertFalse(hasattr(encryption_type, 'id'), + 'encryption type has an id') + + def test_create(self): + """ + Unit test for VolumeEncryptionTypesManager.create + + Verify that one POST request is made for the encryption type creation. + Verify that encryption type creation returns a VolumeEncryptionType. + """ + result = cs.volume_encryption_types.create(2, {'encryption': + {'provider': 'Test', + 'key_size': None, + 'cipher': None, + 'control_location': + None}}) + cs.assert_called('POST', '/types/2/encryption') + self.assertIsInstance(result, VolumeEncryptionType) + + def test_update(self): + """ + Unit test for VolumeEncryptionTypesManager.update + """ + self.skipTest("Not implemented") + + def test_delete(self): + """ + Unit test for VolumeEncryptionTypesManager.delete + """ + self.skipTest("Not implemented") diff --git a/cinderclient/tests/v1/test_volume_transfers.py b/cinderclient/tests/v1/test_volume_transfers.py index 40fb09b..47656d7 100644 --- a/cinderclient/tests/v1/test_volume_transfers.py +++ b/cinderclient/tests/v1/test_volume_transfers.py @@ -20,7 +20,7 @@ from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() -class VolumeTRansfersTest(utils.TestCase): +class VolumeTransfersTest(utils.TestCase): def test_create(self): cs.transfers.create('1234') diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index 0da88e2..2da7509 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -87,3 +87,7 @@ class VolumesTest(utils.TestCase): v = cs.volumes.get('1234') cs.volumes.extend(v, 2) cs.assert_called('POST', '/volumes/1234/action') + + def test_get_encryption_metadata(self): + cs.volumes.get_encryption_metadata('1234') + cs.assert_called('GET', '/volumes/1234/encryption') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 8f70e09..f9a20a8 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -283,6 +283,10 @@ class FakeHTTPClient(base_client.HTTPClient): r = {'volume': self.get_volumes_detail()[2]['volumes'][0]} return (200, {}, r) + def get_volumes_1234_encryption(self, **kw): + r = {'encryption_key_id': 'id'} + return (200, {}, r) + def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 @@ -390,6 +394,11 @@ class FakeHTTPClient(base_client.HTTPClient): 'name': 'test-type-1', 'extra_specs': {}}}) + def get_types_2(self, **kw): + return (200, {}, {'volume_type': {'id': 2, + 'name': 'test-type-2', + 'extra_specs': {}}}) + def post_types(self, body, **kw): return (202, {}, {'volume_type': {'id': 3, 'name': 'test-type-3', @@ -405,6 +414,23 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_types_1(self, **kw): return (202, {}, None) + # + # VolumeEncryptionTypes + # + def get_types_1_encryption(self, **kw): + return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test', + 'cipher': 'test', 'key_size': 1, + 'control_location': 'front'}) + + def get_types_2_encryption(self, **kw): + return (200, {}, {}) + + def post_types_2_encryption(self, body, **kw): + return (200, {}, {'encryption': {}}) + + def put_types_1_encryption_1(self, body, **kw): + return (200, {}, {}) + # # Set/Unset metadata # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 2405192..6f9a53f 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -181,3 +181,60 @@ class ShellTest(utils.TestCase): self.run_command('snapshot-reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + + def test_encryption_type_list(self): + """ + Test encryption-type-list shell command. + + Verify a series of GET requests are made: + - one to get the volume type list information + - one per volume type to retrieve the encryption type information + """ + self.run_command('encryption-type-list') + self.assert_called_anytime('GET', '/types') + self.assert_called_anytime('GET', '/types/1/encryption') + self.assert_called_anytime('GET', '/types/2/encryption') + + def test_encryption_type_show(self): + """ + Test encryption-type-show shell command. + + Verify two GET requests are made per command invocation: + - one to get the volume type information + - one to get the encryption type information + """ + self.run_command('encryption-type-show 1') + self.assert_called('GET', '/types/1/encryption') + self.assert_called_anytime('GET', '/types/1') + + def test_encryption_type_create(self): + """ + Test encryption-type-create shell command. + + Verify GET and POST requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one POST request to create the new encryption type + """ + expected = {'encryption': {'cipher': None, 'key_size': None, + 'provider': 'TestProvider', + 'control_location': None}} + self.run_command('encryption-type-create 2 TestProvider') + self.assert_called('POST', '/types/2/encryption', body=expected) + self.assert_called_anytime('GET', '/types/2') + + def test_encryption_type_update(self): + """ + Test encryption-type-update shell command. + + Verify two GETs/one PUT requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one GET request to retrieve the relevant encryption type information + - one PUT request to update the encryption type information + """ + self.skipTest("Not implemented") + + def test_encryption_type_delete(self): + """ + Test encryption-type-delete shell command. + """ + self.skipTest("Not implemented") diff --git a/cinderclient/tests/v2/test_volume_encryption_types.py b/cinderclient/tests/v2/test_volume_encryption_types.py new file mode 100644 index 0000000..96a0c02 --- /dev/null +++ b/cinderclient/tests/v2/test_volume_encryption_types.py @@ -0,0 +1,95 @@ +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# 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.v2.volume_encryption_types import VolumeEncryptionType +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes + +cs = fakes.FakeClient() + + +class VolumeEncryptionTypesTest(utils.TestCase): + """ + Test suite for the Volume Encryption Types Resource and Manager. + """ + + def test_list(self): + """ + Unit test for VolumeEncryptionTypesManager.list + + Verify that a series of GET requests are made: + - one GET request for the list of volume types + - one GET request per volume type for encryption type information + + Verify that all returned information is :class: VolumeEncryptionType + """ + encryption_types = cs.volume_encryption_types.list() + cs.assert_called_anytime('GET', '/types') + cs.assert_called_anytime('GET', '/types/2/encryption') + cs.assert_called_anytime('GET', '/types/1/encryption') + for encryption_type in encryption_types: + self.assertIsInstance(encryption_type, VolumeEncryptionType) + + def test_get(self): + """ + Unit test for VolumeEncryptionTypesManager.get + + Verify that one GET request is made for the volume type encryption + type information. Verify that returned information is :class: + VolumeEncryptionType + """ + encryption_type = cs.volume_encryption_types.get(1) + cs.assert_called('GET', '/types/1/encryption') + self.assertIsInstance(encryption_type, VolumeEncryptionType) + + def test_get_no_encryption(self): + """ + Unit test for VolumeEncryptionTypesManager.get + + Verify that a request on a volume type with no associated encryption + type information returns a VolumeEncryptionType with no attributes. + """ + encryption_type = cs.volume_encryption_types.get(2) + self.assertIsInstance(encryption_type, VolumeEncryptionType) + self.assertFalse(hasattr(encryption_type, 'id'), + 'encryption type has an id') + + def test_create(self): + """ + Unit test for VolumeEncryptionTypesManager.create + + Verify that one POST request is made for the encryption type creation. + Verify that encryption type creation returns a VolumeEncryptionType. + """ + result = cs.volume_encryption_types.create(2, {'encryption': + {'provider': 'Test', + 'key_size': None, + 'cipher': None, + 'control_location': + None}}) + cs.assert_called('POST', '/types/2/encryption') + self.assertIsInstance(result, VolumeEncryptionType) + + def test_update(self): + """ + Unit test for VolumeEncryptionTypesManager.update + """ + self.skipTest("Not implemented") + + def test_delete(self): + """ + Unit test for VolumeEncryptionTypesManager.delete + """ + self.skipTest("Not implemented") diff --git a/cinderclient/tests/v2/test_volume_transfers.py b/cinderclient/tests/v2/test_volume_transfers.py index 40fb09b..47656d7 100644 --- a/cinderclient/tests/v2/test_volume_transfers.py +++ b/cinderclient/tests/v2/test_volume_transfers.py @@ -20,7 +20,7 @@ from cinderclient.tests.v1 import fakes cs = fakes.FakeClient() -class VolumeTRansfersTest(utils.TestCase): +class VolumeTransfersTest(utils.TestCase): def test_create(self): cs.transfers.create('1234') diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 8a2560d..594bba4 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -90,3 +90,7 @@ class VolumesTest(utils.TestCase): v = cs.volumes.get('1234') cs.volumes.extend(v, 2) cs.assert_called('POST', '/volumes/1234/action') + + def test_get_encryption_metadata(self): + cs.volumes.get_encryption_metadata('1234') + cs.assert_called('GET', '/volumes/1234/encryption') diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 1272c4e..60376ab 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -22,6 +22,7 @@ from cinderclient.v1 import services from cinderclient.v1 import volumes from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volume_types +from cinderclient.v1 import volume_encryption_types from cinderclient.v1 import volume_backups from cinderclient.v1 import volume_backups_restore from cinderclient.v1 import volume_transfers @@ -59,6 +60,8 @@ class Client(object): self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) + self.volume_encryption_types = \ + volume_encryption_types.VolumeEncryptionTypeManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 6c5d266..88e0ec2 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -525,7 +525,7 @@ def do_type_delete(cs, args): help='Extra_specs to set/unset (only key is necessary on unset)') @utils.service_type('volume') def do_type_key(cs, args): - "Set or unset extra_spec for a volume type.""" + """Set or unset extra_spec for a volume type.""" vtype = _find_volume_type(cs, args.vtype) if args.metadata is not None: @@ -947,3 +947,86 @@ def do_availability_zone_list(cs, _args): result += _treeizeAvailabilityZone(zone) _translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status']) + + +def _print_volume_encryption_type_list(encryption_types): + """ + Display a tabularized list of volume encryption types. + + :param encryption_types: a list of :class: VolumeEncryptionType instances + """ + utils.print_list(encryption_types, ['Volume Type ID', 'Provider', + 'Cipher', 'Key Size', + 'Control Location']) + + +@utils.service_type('volume') +def do_encryption_type_list(cs, args): + """List encryption type information for all volume types (Admin Only).""" + result = cs.volume_encryption_types.list() + utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher', + 'Key Size', 'Control Location']) + + +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.service_type('volume') +def do_encryption_type_show(cs, args): + """Show the encryption type information for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + + result = cs.volume_encryption_types.get(volume_type) + + # Display result or an empty table if no result + if hasattr(result, 'volume_type_id'): + _print_volume_encryption_type_list([result]) + else: + _print_volume_encryption_type_list([]) + + +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.arg('provider', + metavar='', + type=str, + help="Class providing encryption support (e.g. LuksEncryptor)") +@utils.arg('--cipher', + metavar='', + type=str, + required=False, + default=None, + help="Encryption algorithm/mode to use (e.g., aes-xts-plain64) " + "(Optional, Default=None)") +@utils.arg('--key_size', + metavar='', + type=int, + required=False, + default=None, + help="Size of the encryption key, in bits (e.g., 128, 256) " + "(Optional, Default=None)") +@utils.arg('--control_location', + metavar='', + choices=['front-end', 'back-end'], + type=str, + required=False, + default=None, + help="Notional service where encryption is performed (e.g., " + "front-end=Nova). Values: 'front-end', 'back-end' " + "(Optional, Default=None)") +@utils.service_type('volume') +def do_encryption_type_create(cs, args): + """Create a new encryption type for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + + body = {} + body['provider'] = args.provider + body['cipher'] = args.cipher + body['key_size'] = args.key_size + body['control_location'] = args.control_location + + result = cs.volume_encryption_types.create(volume_type, body) + _print_volume_encryption_type_list([result]) diff --git a/cinderclient/v1/volume_encryption_types.py b/cinderclient/v1/volume_encryption_types.py new file mode 100644 index 0000000..b97c6f0 --- /dev/null +++ b/cinderclient/v1/volume_encryption_types.py @@ -0,0 +1,96 @@ +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# 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 Encryption Type interface +""" + +from cinderclient import base + + +class VolumeEncryptionType(base.Resource): + """ + A Volume Encryption Type is a collection of settings used to conduct + encryption for a specific volume type. + """ + def __repr__(self): + return "" % self.name + + +class VolumeEncryptionTypeManager(base.ManagerWithFind): + """ + Manage :class: `VolumeEncryptionType` resources. + """ + resource_class = VolumeEncryptionType + + def list(self): + """ + List all volume encryption types. + + :param volume_types: a list of volume types + :return: a list of :class: VolumeEncryptionType instances + """ + # Since the encryption type is a volume type extension, we cannot get + # all encryption types without going through all volume types. + volume_types = self.api.volume_types.list() + encryption_types = [] + for volume_type in volume_types: + encryption_type = self._get("/types/%s/encryption" + % base.getid(volume_type)) + if hasattr(encryption_type, 'volume_type_id'): + encryption_types.append(encryption_type) + return encryption_types + + def get(self, volume_type): + """ + Get the volume encryption type for the specified volume type. + + :param volume_type: the volume type to query + :return: an instance of :class: VolumeEncryptionType + """ + return self._get("/types/%s/encryption" % base.getid(volume_type)) + + def create(self, volume_type, specs): + """ + Create a new encryption type for the specified volume type. + + :param volume_type: the volume type on which to add an encryption type + :param specs: the encryption type specifications to add + :return: an instance of :class: VolumeEncryptionType + """ + body = {'encryption': specs} + return self._create("/types/%s/encryption" % base.getid(volume_type), + body, "encryption") + + def update(self, volume_type, specs): + """ + Update the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be updated + :param specs: the encryption type specifications to update + :return: an instance of :class: VolumeEncryptionType + """ + raise NotImplementedError() + + def delete(self, volume_type): + """ + Delete the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be deleted + """ + raise NotImplementedError() diff --git a/cinderclient/v1/volume_types.py b/cinderclient/v1/volume_types.py index 93eabd3..12c4612 100644 --- a/cinderclient/v1/volume_types.py +++ b/cinderclient/v1/volume_types.py @@ -55,7 +55,7 @@ class VolumeType(base.Resource): def unset_keys(self, keys): """ - Unset extra specs on a volue type. + Unset extra specs on a volume type. :param type_id: The :class:`VolumeType` to unset extra spec on :param keys: A list of keys to be unset diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 9c870cb..6d63e72 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -134,13 +134,13 @@ class VolumeManager(base.ManagerWithFind): :param display_name: Name of the volume :param display_description: Description of the volume :param volume_type: Type of volume - :rtype: :class:`Volume` :param user_id: User id derived from context :param project_id: Project id derived from context :param availability_zone: Availability Zone to use :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 + :rtype: :class:`Volume` """ if metadata is None: @@ -352,3 +352,12 @@ class VolumeManager(base.ManagerWithFind): return self._action('os-extend', base.getid(volume), {'new_size': new_size}) + + def get_encryption_metadata(self, volume_id): + """ + Retrieve the encryption metadata from the desired volume. + + :param volume_id: the id of the volume to query + :return: a dictionary of volume encryption metadata + """ + return self._get("/volumes/%s/encryption" % volume_id)._info diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 31781a8..2f73ed6 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -22,6 +22,7 @@ from cinderclient.v2 import services from cinderclient.v2 import volumes from cinderclient.v2 import volume_snapshots from cinderclient.v2 import volume_types +from cinderclient.v2 import volume_encryption_types from cinderclient.v2 import volume_backups from cinderclient.v2 import volume_backups_restore from cinderclient.v1 import volume_transfers @@ -57,6 +58,8 @@ class Client(object): self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) + self.volume_encryption_types = \ + volume_encryption_types.VolumeEncryptionTypeManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index a1c1f22..8a1900c 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1032,3 +1032,86 @@ def do_availability_zone_list(cs, _args): result += _treeizeAvailabilityZone(zone) _translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status']) + + +def _print_volume_encryption_type_list(encryption_types): + """ + Display a tabularized list of volume encryption types. + + :param encryption_types: a list of :class: VolumeEncryptionType instances + """ + utils.print_list(encryption_types, ['Volume Type ID', 'Provider', + 'Cipher', 'Key Size', + 'Control Location']) + + +@utils.service_type('volumev2') +def do_encryption_type_list(cs, args): + """List encryption type information for all volume types (Admin Only).""" + result = cs.volume_encryption_types.list() + utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher', + 'Key Size', 'Control Location']) + + +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.service_type('volumev2') +def do_encryption_type_show(cs, args): + """Show the encryption type information for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + + result = cs.volume_encryption_types.get(volume_type) + + # Display result or an empty table if no result + if hasattr(result, 'volume_type_id'): + _print_volume_encryption_type_list([result]) + else: + _print_volume_encryption_type_list([]) + + +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.arg('provider', + metavar='', + type=str, + help="Class providing encryption support (e.g. LuksEncryptor)") +@utils.arg('--cipher', + metavar='', + type=str, + required=False, + default=None, + help="Encryption algorithm/mode to use (e.g., aes-xts-plain64) " + "(Optional, Default=None)") +@utils.arg('--key_size', + metavar='', + type=int, + required=False, + default=None, + help="Size of the encryption key, in bits (e.g., 128, 256) " + "(Optional, Default=None)") +@utils.arg('--control_location', + metavar='', + choices=['front-end', 'back-end'], + type=str, + required=False, + default=None, + help="Notional service where encryption is performed (e.g., " + "front-end=Nova). Values: 'front-end', 'back-end' " + "(Optional, Default=None)") +@utils.service_type('volumev2') +def do_encryption_type_create(cs, args): + """Create a new encryption type for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + + body = {} + body['provider'] = args.provider + body['cipher'] = args.cipher + body['key_size'] = args.key_size + body['control_location'] = args.control_location + + result = cs.volume_encryption_types.create(volume_type, body) + _print_volume_encryption_type_list([result]) diff --git a/cinderclient/v2/volume_encryption_types.py b/cinderclient/v2/volume_encryption_types.py new file mode 100644 index 0000000..b97c6f0 --- /dev/null +++ b/cinderclient/v2/volume_encryption_types.py @@ -0,0 +1,96 @@ +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# 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 Encryption Type interface +""" + +from cinderclient import base + + +class VolumeEncryptionType(base.Resource): + """ + A Volume Encryption Type is a collection of settings used to conduct + encryption for a specific volume type. + """ + def __repr__(self): + return "" % self.name + + +class VolumeEncryptionTypeManager(base.ManagerWithFind): + """ + Manage :class: `VolumeEncryptionType` resources. + """ + resource_class = VolumeEncryptionType + + def list(self): + """ + List all volume encryption types. + + :param volume_types: a list of volume types + :return: a list of :class: VolumeEncryptionType instances + """ + # Since the encryption type is a volume type extension, we cannot get + # all encryption types without going through all volume types. + volume_types = self.api.volume_types.list() + encryption_types = [] + for volume_type in volume_types: + encryption_type = self._get("/types/%s/encryption" + % base.getid(volume_type)) + if hasattr(encryption_type, 'volume_type_id'): + encryption_types.append(encryption_type) + return encryption_types + + def get(self, volume_type): + """ + Get the volume encryption type for the specified volume type. + + :param volume_type: the volume type to query + :return: an instance of :class: VolumeEncryptionType + """ + return self._get("/types/%s/encryption" % base.getid(volume_type)) + + def create(self, volume_type, specs): + """ + Create a new encryption type for the specified volume type. + + :param volume_type: the volume type on which to add an encryption type + :param specs: the encryption type specifications to add + :return: an instance of :class: VolumeEncryptionType + """ + body = {'encryption': specs} + return self._create("/types/%s/encryption" % base.getid(volume_type), + body, "encryption") + + def update(self, volume_type, specs): + """ + Update the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be updated + :param specs: the encryption type specifications to update + :return: an instance of :class: VolumeEncryptionType + """ + raise NotImplementedError() + + def delete(self, volume_type): + """ + Delete the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be deleted + """ + raise NotImplementedError() diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 14535af..be4a9e6 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -129,7 +129,6 @@ class VolumeManager(base.ManagerWithFind): :param name: Name of the volume :param description: Description of the volume :param volume_type: Type of volume - :rtype: :class:`Volume` :param user_id: User id derived from context :param project_id: Project id derived from context :param availability_zone: Availability Zone to use @@ -138,6 +137,7 @@ class VolumeManager(base.ManagerWithFind): :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 + :rtype: :class:`Volume` """ if metadata is None: @@ -334,3 +334,12 @@ class VolumeManager(base.ManagerWithFind): return self._action('os-extend', base.getid(volume), {'new_size': new_size}) + + def get_encryption_metadata(self, volume_id): + """ + Retrieve the encryption metadata from the desired volume. + + :param volume_id: the id of the volume to query + :return: a dictionary of volume encryption metadata + """ + return self._get("/volumes/%s/encryption" % volume_id)._info From 38e1d061e8cbec45a7cb9e7c8acd1ad254b0acfb Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Tue, 23 Jul 2013 12:51:49 -0400 Subject: [PATCH 086/165] Add update_snapshot_metadata action This allows Nova to update the state and progress of a snapshot that it is manipulating. Also fix formatting bug in AssertionError in post_snapshots_1234_action. Implements blueprint qemu-assisted-snapshots Change-Id: Ia108e14870410b783c5d074db89acb94e83fce99 --- cinderclient/tests/v1/fakes.py | 4 ++- .../tests/v1/test_snapshot_actions.py | 35 +++++++++++++++++++ cinderclient/tests/v2/fakes.py | 4 ++- .../tests/v2/test_snapshot_actions.py | 35 +++++++++++++++++++ cinderclient/v1/volume_snapshots.py | 4 +++ cinderclient/v2/volume_snapshots.py | 4 +++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 cinderclient/tests/v1/test_snapshot_actions.py create mode 100644 cinderclient/tests/v2/test_snapshot_actions.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index c83f5ad..2c4eaa4 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -244,8 +244,10 @@ class FakeHTTPClient(base_client.HTTPClient): action = body.keys()[0] if action == 'os-reset_status': assert 'status' in body['os-reset_status'] + elif action == 'os-update_snapshot_status': + assert 'status' in body['os-update_snapshot_status'] else: - raise AssertionError('Unexpected action: %s" % action') + raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) # diff --git a/cinderclient/tests/v1/test_snapshot_actions.py b/cinderclient/tests/v1/test_snapshot_actions.py new file mode 100644 index 0000000..70b14e1 --- /dev/null +++ b/cinderclient/tests/v1/test_snapshot_actions.py @@ -0,0 +1,35 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class SnapshotActionsTest(utils.TestCase): + def test_update_snapshot_status(self): + s = cs.volume_snapshots.get('1234') + cs.volume_snapshots.update_snapshot_status(s, + {'status': 'available'}) + cs.assert_called('POST', '/snapshots/1234/action') + + def test_update_snapshot_status_with_progress(self): + s = cs.volume_snapshots.get('1234') + cs.volume_snapshots.update_snapshot_status(s, + {'status': 'available', + 'progress': '73%'}) + cs.assert_called('POST', '/snapshots/1234/action') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 8f70e09..82fb6e2 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -251,8 +251,10 @@ class FakeHTTPClient(base_client.HTTPClient): action = body.keys()[0] if action == 'os-reset_status': assert 'status' in body['os-reset_status'] + elif action == 'os-update_snapshot_status': + assert 'status' in body['os-update_snapshot_status'] else: - raise AssertionError('Unexpected action: %s" % action') + raise AssertionError('Unexpected action: %s' % action) return (resp, {}, _body) # diff --git a/cinderclient/tests/v2/test_snapshot_actions.py b/cinderclient/tests/v2/test_snapshot_actions.py new file mode 100644 index 0000000..f70cc8f --- /dev/null +++ b/cinderclient/tests/v2/test_snapshot_actions.py @@ -0,0 +1,35 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes + + +cs = fakes.FakeClient() + + +class SnapshotActionsTest(utils.TestCase): + def test_update_snapshot_status(self): + s = cs.volume_snapshots.get('1234') + cs.volume_snapshots.update_snapshot_status(s, + {'status': 'available'}) + cs.assert_called('POST', '/snapshots/1234/action') + + def test_update_snapshot_status_with_progress(self): + s = cs.volume_snapshots.get('1234') + cs.volume_snapshots.update_snapshot_status(s, + {'status': 'available', + 'progress': '73%'}) + cs.assert_called('POST', '/snapshots/1234/action') diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index 63e92c8..0aa6495 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -148,3 +148,7 @@ class SnapshotManager(base.ManagerWithFind): self.run_hooks('modify_body_for_action', body, **kwargs) url = '/snapshots/%s/action' % base.getid(snapshot) return self.api.client.post(url, body=body) + + def update_snapshot_status(self, snapshot, update_dict): + return self._action('os-update_snapshot_status', + base.getid(snapshot), update_dict) diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index ef529eb..7aa9097 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -133,3 +133,7 @@ class SnapshotManager(base.ManagerWithFind): self.run_hooks('modify_body_for_action', body, **kwargs) url = '/snapshots/%s/action' % base.getid(snapshot) return self.api.client.post(url, body=body) + + def update_snapshot_status(self, snapshot, update_dict): + return self._action('os-update_snapshot_status', + base.getid(snapshot), update_dict) From b757c348b758fb5517cc0a08675b6c3ae8a7538e Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Tue, 3 Sep 2013 04:39:24 +0800 Subject: [PATCH 087/165] Don't need to init testr explicitly In run_tests.sh, function init_testr will initialize testr if the directory .testrepository is not existed. Actually, testr will do the check before run the test: In Python package testrepository, setuptools_command.py:Testr.run 68 def run(self): 69 """Set up testr repo, then run testr""" 70 if not os.path.isdir(".testrepository"): 71 self._run_testr("init") So, init_testr can be removed safely. Fixes Bug #1220147 Change-Id: Ide99a836cd601453624c7a562b7256c86bd46811 --- run_tests.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 3299a30..017f7e2 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -102,11 +102,6 @@ if [ $no_site_packages -eq 1 ]; then installvenvopts="--no-site-packages" fi -function init_testr { - if [ ! -d .testrepository ]; then - ${wrapper} testr init - fi -} function run_tests { # Cleanup *pyc @@ -223,7 +218,6 @@ 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, From 07bd82488d9985c237cf630352cb06ebba1ea789 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 5 Sep 2013 09:59:41 -0700 Subject: [PATCH 088/165] Added support for running the tests under PyPy with tox This is a precursor to having them run under check and gate. Change-Id: I377f0a4dddc063402db7486789f7c00847013eec --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 65bcb21..f4debdd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distribute = False -envlist = py26,py27,py33,pep8 +envlist = py26,py27,py33,pypy,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} From af83fcc798ba2e3e6c831688e66801b47c65f60a Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Tue, 13 Aug 2013 22:38:14 +0200 Subject: [PATCH 089/165] Now build-depends python-pbr (>= 0.5.20), (build-)depends: python-six, and using testrepository --- debian/changelog | 3 +++ debian/control | 9 ++++++++- debian/rules | 4 +--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 4b59dd9..fd81a13 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,9 @@ python-cinderclient (1:1.0.5-1) unstable; urgency=low * New upstream release. * Ran wrap-and-sort. + * Now build-depends python-pbr (>= 0.5.20), testrepository. + * Unit tests are now using testrepository. + * Added (build-)depends: python-six. -- Thomas Goirand Thu, 30 May 2013 13:42:32 +0800 diff --git a/debian/control b/debian/control index c9a8a72..ef8e5ed 100644 --- a/debian/control +++ b/debian/control @@ -12,13 +12,17 @@ Build-Depends-Indep: openstack-pkg-tools, python-httplib2, python-mock, python-nose, + python-pbr (>= 0.5.21), python-prettytable, + python-requests (>= 1.1), python-setuptools, python-setuptools-git, python-simplejson, + python-six, python-sphinx, python-subunit, - python-unittest2 + python-unittest2, + testrepository (>= 0.0.17) Standards-Version: 3.9.3 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary Vcs-Git: git://anonscm.debian.org/openstack/python-cinderclient.git @@ -28,8 +32,11 @@ Package: python-cinderclient Architecture: all Pre-Depends: dpkg (>= 1.15.6~) Depends: python-httplib2, + python-pbr (>= 0.5.21), python-prettytable, + python-requests (>= 1.1), python-simplejson, + python-six, ${misc:Depends}, ${python:Depends} Provides: ${python:Provides} diff --git a/debian/rules b/debian/rules index a5e5ece..9c1bdef 100755 --- a/debian/rules +++ b/debian/rules @@ -27,7 +27,5 @@ override_dh_install: ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) override_dh_auto_test: - for i in `pyversions -s` ; do \ - $$i -m subunit.run discover -t ./ ./tests ; \ - done + python setup.py testr endif From 86d4d3190d6973fb64bcdf0807d5db6fdd6f51b8 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 11 Sep 2013 16:54:50 +0800 Subject: [PATCH 090/165] Edited debian/changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index fd81a13..8ebbcec 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.5-2) unstable; urgency=low + + * Added missing python-requests (build-)depends. + + -- Thomas Goirand Wed, 11 Sep 2013 16:54:23 +0800 + python-cinderclient (1:1.0.5-1) unstable; urgency=low * New upstream release. From 79274483f4f72ffa1868ef0ce58fbc53ad27a6cf Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 11 Sep 2013 16:57:14 +0800 Subject: [PATCH 091/165] Added cleaner=true to avoid missing python-pbr on auto_clean --- debian/gbp.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/gbp.conf b/debian/gbp.conf index ccf8702..08509a2 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -6,3 +6,4 @@ compression = xz [git-buildpackage] export-dir = ../build-area/ +cleaner = true From 2ac8f3a337130a7d130fb49bfacbf1842d3e5e47 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Fri, 6 Sep 2013 13:17:36 +0900 Subject: [PATCH 092/165] Fix help messages for name arguments Users can specify the name of an instance as the argument, which are passed through find_resource(), instead of the id. This patch changes some help messages for explaining this behavior. Related-Bug: #1220590 Change-Id: I9af1259af4319b82b94d7b28536def3107ec5dd5 --- cinderclient/v1/shell.py | 59 ++++++++++++++++++++++------------------ cinderclient/v2/shell.py | 55 +++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 91c2046..a4d4cb1 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -61,22 +61,22 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, def _find_volume(cs, volume): - """Get a volume by ID.""" + """Get a volume by name or ID.""" return utils.find_resource(cs.volumes, volume) def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by ID.""" + """Get a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) def _find_backup(cs, backup): - """Get a backup by ID.""" + """Get a backup by name or ID.""" return utils.find_resource(cs.backups, backup) def _find_transfer(cs, transfer): - """Get a transfer by ID.""" + """Get a transfer by name or ID.""" return utils.find_resource(cs.transfers, transfer) @@ -182,7 +182,7 @@ def do_list(cs, args): 'Size', 'Volume Type', 'Bootable', 'Attached to']) -@utils.arg('volume', metavar='', help='ID of the volume.') +@utils.arg('volume', metavar='', help='Name or ID of the volume.') @utils.service_type('volume') def do_show(cs, args): """Show details about a volume.""" @@ -276,7 +276,8 @@ def do_create(cs, args): _print_volume(volume) -@utils.arg('volume', metavar='', help='ID of the volume to delete.') +@utils.arg('volume', metavar='', + help='Name or ID of the volume to delete.') @utils.service_type('volume') def do_delete(cs, args): """Remove a volume.""" @@ -284,7 +285,8 @@ def do_delete(cs, args): volume.delete() -@utils.arg('volume', metavar='', help='ID of the volume to delete.') +@utils.arg('volume', metavar='', + help='Name or ID of the volume to delete.') @utils.service_type('volume') def do_force_delete(cs, args): """Attempt forced removal of a volume, regardless of its state.""" @@ -292,7 +294,8 @@ def do_force_delete(cs, args): volume.force_delete() -@utils.arg('volume', metavar='', help='ID of the volume to modify.') +@utils.arg('volume', metavar='', + help='Name or 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 ' @@ -304,7 +307,8 @@ def do_reset_state(cs, args): volume.reset_state(args.state) -@utils.arg('volume', metavar='', help='ID of the volume to rename.') +@utils.arg('volume', metavar='', + help='Name or ID of the volume to rename.') @utils.arg('display_name', nargs='?', metavar='', help='New display-name for the volume.') @utils.arg('--display-description', metavar='', @@ -323,7 +327,7 @@ def do_rename(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to update metadata on.') + help='Name or ID of the volume to update metadata on.') @utils.arg('action', metavar='', choices=['set', 'unset'], @@ -392,7 +396,8 @@ def do_snapshot_list(cs, args): ['ID', 'Volume ID', 'Status', 'Display Name', 'Size']) -@utils.arg('snapshot', metavar='', help='ID of the snapshot.') +@utils.arg('snapshot', metavar='', + help='Name or ID of the snapshot.') @utils.service_type('volume') def do_snapshot_show(cs, args): """Show details about a snapshot.""" @@ -435,17 +440,18 @@ def do_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('snapshot_id', - metavar='', - help='ID of the snapshot to delete.') +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot to delete.') @utils.service_type('volume') def do_snapshot_delete(cs, args): """Remove a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot_id) + snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot.delete() -@utils.arg('snapshot', metavar='', help='ID of the snapshot.') +@utils.arg('snapshot', metavar='', + help='Name or ID of the snapshot.') @utils.arg('display_name', nargs='?', metavar='', help='New display-name for the snapshot.') @utils.arg('--display-description', metavar='', @@ -463,7 +469,7 @@ def do_snapshot_rename(cs, args): @utils.arg('snapshot', metavar='', - help='ID of the snapshot to modify.') + help='Name or ID of the snapshot to modify.') @utils.arg('--state', metavar='', default='available', help=('Indicate which state to assign the snapshot. ' @@ -693,9 +699,9 @@ def _find_volume_type(cs, vtype): return utils.find_resource(cs.volume_types, vtype) -@utils.arg('volume_id', - metavar='', - help='ID of the volume to upload to an image') +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to upload to an image') @utils.arg('--force', metavar='', help='Optional flag to indicate whether ' @@ -718,7 +724,7 @@ def _find_volume_type(cs, vtype): @utils.service_type('volume') def do_upload_to_image(cs, args): """Upload volume to image service as image.""" - volume = _find_volume(cs, args.volume_id) + volume = _find_volume(cs, args.volume) _print_volume_image(volume.upload_to_image(args.force, args.image_name, args.container_format, @@ -753,7 +759,7 @@ def do_backup_create(cs, args): utils.print_dict(info) -@utils.arg('backup', metavar='', help='ID of the backup.') +@utils.arg('backup', metavar='', help='Name or ID of the backup.') @utils.service_type('volume') def do_backup_show(cs, args): """Show details about a backup.""" @@ -777,7 +783,7 @@ def do_backup_list(cs, args): @utils.arg('backup', metavar='', - help='ID of the backup to delete.') + help='Name or ID of the backup to delete.') @utils.service_type('volume') def do_backup_delete(cs, args): """Remove a backup.""" @@ -817,7 +823,7 @@ def do_transfer_create(cs, args): @utils.arg('transfer', metavar='', - help='ID of the transfer to delete.') + help='Name or ID of the transfer to delete.') @utils.service_type('volume') def do_transfer_delete(cs, args): """Undo a transfer.""" @@ -851,7 +857,7 @@ def do_transfer_list(cs, args): @utils.arg('transfer', metavar='', - help='ID of the transfer to accept.') + help='Name or ID of the transfer to accept.') @utils.service_type('volume') def do_transfer_show(cs, args): """Show details about a transfer.""" @@ -865,7 +871,8 @@ def do_transfer_show(cs, args): utils.print_dict(info) -@utils.arg('volume', metavar='', help='ID of the volume to extend.') +@utils.arg('volume', metavar='', + help='Name or ID of the volume to extend.') @utils.arg('new_size', metavar='', type=int, diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6e1adbc..ff17a5e 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -59,22 +59,22 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, def _find_volume(cs, volume): - """Get a volume by ID.""" + """Get a volume by name or ID.""" return utils.find_resource(cs.volumes, volume) def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by ID.""" + """Get a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) def _find_backup(cs, backup): - """Get a backup by ID.""" + """Get a backup by name or ID.""" return utils.find_resource(cs.backups, backup) def _find_transfer(cs, transfer): - """Get a transfer by ID.""" + """Get a transfer by name or ID.""" return utils.find_resource(cs.transfers, transfer) @@ -180,7 +180,7 @@ def do_list(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume.') + help='Name or ID of the volume.') @utils.service_type('volumev2') def do_show(cs, args): """Show details about a volume.""" @@ -304,7 +304,7 @@ def do_create(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to delete.') + help='Name or ID of the volume to delete.') @utils.service_type('volumev2') def do_delete(cs, args): """Remove a volume.""" @@ -314,7 +314,7 @@ def do_delete(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to delete.') + help='Name or ID of the volume to delete.') @utils.service_type('volumev2') def do_force_delete(cs, args): """Attempt forced removal of a volume, regardless of its state.""" @@ -322,7 +322,8 @@ def do_force_delete(cs, args): volume.force_delete() -@utils.arg('volume', metavar='', help='ID of the volume to modify.') +@utils.arg('volume', metavar='', + help='Name or 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 ' @@ -336,7 +337,7 @@ def do_reset_state(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to rename.') + help='Name or ID of the volume to rename.') @utils.arg('name', nargs='?', metavar='', @@ -365,7 +366,7 @@ def do_rename(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to update metadata on.') + help='Name or ID of the volume to update metadata on.') @utils.arg('action', metavar='', choices=['set', 'unset'], @@ -442,7 +443,7 @@ def do_snapshot_list(cs, args): @utils.arg('snapshot', metavar='', - help='ID of the snapshot.') + help='Name or ID of the snapshot.') @utils.service_type('volumev2') def do_snapshot_show(cs, args): """Show details about a snapshot.""" @@ -491,17 +492,18 @@ def do_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('snapshot-id', - metavar='', - help='ID of the snapshot to delete.') +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot to delete.') @utils.service_type('volumev2') def do_snapshot_delete(cs, args): """Remove a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot_id) + snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot.delete() -@utils.arg('snapshot', metavar='', help='ID of the snapshot.') +@utils.arg('snapshot', metavar='', + help='Name or ID of the snapshot.') @utils.arg('name', nargs='?', metavar='', help='New name for the snapshot.') @utils.arg('--description', metavar='', @@ -528,7 +530,7 @@ def do_snapshot_rename(cs, args): @utils.arg('snapshot', metavar='', - help='ID of the snapshot to modify.') + help='Name or ID of the snapshot to modify.') @utils.arg('--state', metavar='', default='available', help=('Indicate which state to assign the snapshot. ' @@ -762,9 +764,9 @@ def _find_volume_type(cs, vtype): return utils.find_resource(cs.volume_types, vtype) -@utils.arg('volume-id', - metavar='', - help='ID of the volume to snapshot') +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to snapshot') @utils.arg('--force', metavar='', help='Optional flag to indicate whether ' @@ -793,7 +795,7 @@ def _find_volume_type(cs, vtype): @utils.service_type('volumev2') def do_upload_to_image(cs, args): """Upload volume to image service as image.""" - volume = _find_volume(cs, args.volume_id) + volume = _find_volume(cs, args.volume) _print_volume_image(volume.upload_to_image(args.force, args.image_name, args.container_format, @@ -839,7 +841,7 @@ def do_backup_create(cs, args): utils.print_dict(info) -@utils.arg('backup', metavar='', help='ID of the backup.') +@utils.arg('backup', metavar='', help='Name or ID of the backup.') @utils.service_type('volumev2') def do_backup_show(cs, args): """Show details about a backup.""" @@ -861,7 +863,7 @@ def do_backup_list(cs, args): @utils.arg('backup', metavar='', - help='ID of the backup to delete.') + help='Name or ID of the backup to delete.') @utils.service_type('volumev2') def do_backup_delete(cs, args): """Remove a backup.""" @@ -905,7 +907,7 @@ def do_transfer_create(cs, args): @utils.arg('transfer', metavar='', - help='ID of the transfer to delete.') + help='Name or ID of the transfer to delete.') @utils.service_type('volumev2') def do_transfer_delete(cs, args): """Undo a transfer.""" @@ -937,7 +939,7 @@ def do_transfer_list(cs, args): @utils.arg('transfer', metavar='', - help='ID of the transfer to accept.') + help='Name or ID of the transfer to accept.') @utils.service_type('volumev2') def do_transfer_show(cs, args): """Show details about a transfer.""" @@ -949,7 +951,8 @@ def do_transfer_show(cs, args): utils.print_dict(info) -@utils.arg('volume', metavar='', help='ID of the volume to extend.') +@utils.arg('volume', metavar='', + help='Name or ID of the volume to extend.') @utils.arg('new-size', metavar='', type=int, From d7796ef737b0b9c70fabf1416b42ad42c2d27b4a Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Thu, 18 Jul 2013 16:17:21 +0300 Subject: [PATCH 093/165] Implement ability to migrate volume Implements ability to call migrate_volume and migrate_volume_completion APIs. The former includes shell code while the latter is called by Nova and should not be invoked via shell. Change-Id: I6e81d7a6321f367a356f0a0dee385221363a4227 --- cinderclient/tests/v1/fakes.py | 3 +++ cinderclient/tests/v1/test_volumes.py | 5 ++++ cinderclient/tests/v2/fakes.py | 3 +++ cinderclient/tests/v2/test_volumes.py | 5 ++++ cinderclient/v1/shell.py | 14 +++++++++++ cinderclient/v1/volumes.py | 34 +++++++++++++++++++++++++++ cinderclient/v2/shell.py | 14 +++++++++++ cinderclient/v2/volumes.py | 34 +++++++++++++++++++++++++++ 8 files changed, 112 insertions(+) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 69f3d1f..40ddd4c 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -308,6 +308,9 @@ class FakeHTTPClient(base_client.HTTPClient): assert 'status' in body[action] elif action == 'os-extend': assert body[action].keys() == ['new_size'] + elif action == 'os-migrate_volume': + assert 'host' in body[action] + assert 'force_host_copy' in body[action] 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 2da7509..bb73438 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -91,3 +91,8 @@ class VolumesTest(utils.TestCase): def test_get_encryption_metadata(self): cs.volumes.get_encryption_metadata('1234') cs.assert_called('GET', '/volumes/1234/encryption') + + def test_migrate(self): + v = cs.volumes.get('1234') + cs.volumes.migrate_volume(v, 'dest', False) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 745fd39..474d10c 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -315,6 +315,9 @@ class FakeHTTPClient(base_client.HTTPClient): assert 'status' in body[action] elif action == 'os-extend': assert body[action].keys() == ['new_size'] + elif action == 'os-migrate_volume': + assert 'host' in body[action] + assert 'force_host_copy' in body[action] 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 594bba4..73ee4a8 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -94,3 +94,8 @@ class VolumesTest(utils.TestCase): def test_get_encryption_metadata(self): cs.volumes.get_encryption_metadata('1234') cs.assert_called('GET', '/volumes/1234/encryption') + + def test_migrate(self): + v = cs.volumes.get('1234') + cs.volumes.migrate_volume(v, 'dest', False) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 91c2046..12331ef 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1046,3 +1046,17 @@ def do_encryption_type_create(cs, args): result = cs.volume_encryption_types.create(volume_type, body) _print_volume_encryption_type_list([result]) + + +@utils.arg('volume', metavar='', help='ID of the volume to migrate') +@utils.arg('host', metavar='', help='Destination host') +@utils.arg('--force-host-copy', metavar='', + help='Optional flag to force the use of the generic ' + 'host-based migration mechanism, bypassing driver ' + 'optimizations (Default=False).', + default=False) +@utils.service_type('volume') +def do_migrate(cs, args): + """Migrate the volume to the new host.""" + volume = _find_volume(cs, args.volume) + volume.migrate_volume(args.host, args.force_host_copy) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 6d63e72..6804d59 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -114,6 +114,15 @@ class Volume(base.Resource): self.manager.extend(self, volume, new_size) + def migrate_volume(self, host, force_host_copy): + """Migrate the volume to a new host.""" + self.manager.migrate_volume(self, host, force_host_copy) + +# def migrate_volume_completion(self, old_volume, new_volume, error): +# """Complete the migration of the volume.""" +# self.manager.migrate_volume_completion(self, old_volume, +# new_volume, error) + class VolumeManager(base.ManagerWithFind): """ @@ -361,3 +370,28 @@ class VolumeManager(base.ManagerWithFind): :return: a dictionary of volume encryption metadata """ return self._get("/volumes/%s/encryption" % volume_id)._info + + def migrate_volume(self, volume, host, force_host_copy): + """Migrate volume to new host. + + :param volume: The :class:`Volume` to migrate + :param host: The destination host + :param force_host_copy: Skip driver optimizations + """ + + return self._action('os-migrate_volume', + volume, + {'host': host, 'force_host_copy': force_host_copy}) + + def migrate_volume_completion(self, old_volume, new_volume, error): + """Complete the migration from the old volume to the temp new one. + + :param old_volume: The original :class:`Volume` in the migration + :param new_volume: The new temporary :class:`Volume` in the migration + :param error: Inform of an error to cause migration cleanup + """ + + new_volume_id = base.getid(new_volume) + return self._action('os-migrate_volume_completion', + old_volume, + {'new_volume': new_volume_id, 'error': error})[1] diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6e1adbc..9515bca 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -800,6 +800,20 @@ def do_upload_to_image(cs, args): args.disk_format)) +@utils.arg('volume', metavar='', help='ID of the volume to migrate') +@utils.arg('host', metavar='', help='Destination host') +@utils.arg('--force-host-copy', metavar='', + help='Optional flag to force the use of the generic ' + 'host-based migration mechanism, bypassing driver ' + 'optimizations (Default=False).', + default=False) +@utils.service_type('volume') +def do_migrate(cs, args): + """Migrate the volume to the new host.""" + volume = _find_volume(cs, args.volume) + volume.migrate_volume(args.host, args.force_host_copy) + + @utils.arg('volume', metavar='', help='ID of the volume to backup.') @utils.arg('--container', metavar='', diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index be4a9e6..2ec885d 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -112,6 +112,15 @@ class Volume(base.Resource): self.manager.extend(self, volume, new_size) + def migrate_volume(self, host, force_host_copy): + """Migrate the volume to a new host.""" + self.manager.migrate_volume(self, host, force_host_copy) + +# def migrate_volume_completion(self, old_volume, new_volume, error): +# """Complete the migration of the volume.""" +# self.manager.migrate_volume_completion(self, old_volume, +# new_volume, error) + class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" @@ -343,3 +352,28 @@ class VolumeManager(base.ManagerWithFind): :return: a dictionary of volume encryption metadata """ return self._get("/volumes/%s/encryption" % volume_id)._info + + def migrate_volume(self, volume, host, force_host_copy): + """Migrate volume to new host. + + :param volume: The :class:`Volume` to migrate + :param host: The destination host + :param force_host_copy: Skip driver optimizations + """ + + return self._action('os-migrate_volume', + volume, + {'host': host, 'force_host_copy': force_host_copy}) + + def migrate_volume_completion(self, old_volume, new_volume, error): + """Complete the migration from the old volume to the temp new one. + + :param old_volume: The original :class:`Volume` in the migration + :param new_volume: The new temporary :class:`Volume` in the migration + :param error: Inform of an error to cause migration cleanup + """ + + new_volume_id = base.getid(new_volume) + return self._action('os-migrate_volume_completion', + old_volume, + {'new_volume': new_volume_id, 'error': error})[1] From 405702c8c0f3befab31bdfe92fd10f408f8b5a0c Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Wed, 18 Sep 2013 16:44:51 +0900 Subject: [PATCH 094/165] Add volume name arguments This patch adds volume name arguments to the following subcommands: * snapshot-create * backup-create * backup-restore * transfer-create Fixes bug #1220590 Change-Id: Ib0ff6e62d45abb14fa8c7313511ef6f72befe0d5 --- cinderclient/utils.py | 5 ++++ cinderclient/v1/shell.py | 53 ++++++++++++++++++++-------------------- cinderclient/v2/shell.py | 53 ++++++++++++++++++++-------------------- 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 93be477..c7e4ebc 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -230,6 +230,11 @@ def find_resource(manager, name_or_id): raise exceptions.CommandError(msg) +def find_volume(cs, volume): + """Get a volume by name or ID.""" + return find_resource(cs.volumes, volume) + + def _format_servers_list_networks(server): output = [] for (network, addresses) in list(server.networks.items()): diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index a4d4cb1..db6b23b 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -60,11 +60,6 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, time.sleep(poll_period) -def _find_volume(cs, volume): - """Get a volume by name or ID.""" - return utils.find_resource(cs.volumes, volume) - - def _find_volume_snapshot(cs, snapshot): """Get a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) @@ -186,7 +181,7 @@ def do_list(cs, args): @utils.service_type('volume') def do_show(cs, args): """Show details about a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) _print_volume(volume) @@ -281,7 +276,7 @@ def do_create(cs, args): @utils.service_type('volume') def do_delete(cs, args): """Remove a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.delete() @@ -290,7 +285,7 @@ def do_delete(cs, args): @utils.service_type('volume') def do_force_delete(cs, args): """Attempt forced removal of a volume, regardless of its state.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.force_delete() @@ -303,7 +298,7 @@ def do_force_delete(cs, args): @utils.service_type('volume') def do_reset_state(cs, args): """Explicitly update the state of a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.reset_state(args.state) @@ -322,7 +317,7 @@ def do_rename(cs, args): kwargs['display_name'] = args.display_name if args.display_description is not None: kwargs['display_description'] = args.display_description - _find_volume(cs, args.volume).update(**kwargs) + utils.find_volume(cs, args.volume).update(**kwargs) @utils.arg('volume', @@ -340,7 +335,7 @@ def do_rename(cs, args): @utils.service_type('volume') def do_metadata(cs, args): """Set or Delete metadata on a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) metadata = _extract_metadata(args) if args.action == 'set': @@ -405,9 +400,9 @@ def do_snapshot_show(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('volume_id', - metavar='', - help='ID of the volume to snapshot') +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to snapshot') @utils.arg('--force', metavar='', help='Optional flag to indicate whether ' @@ -433,7 +428,8 @@ def do_snapshot_show(cs, args): @utils.service_type('volume') def do_snapshot_create(cs, args): """Add a new snapshot.""" - snapshot = cs.volume_snapshots.create(args.volume_id, + volume = utils.find_volume(cs, args.volume) + snapshot = cs.volume_snapshots.create(volume.id, args.force, args.display_name, args.display_description) @@ -724,7 +720,7 @@ def _find_volume_type(cs, vtype): @utils.service_type('volume') def do_upload_to_image(cs, args): """Upload volume to image service as image.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) _print_volume_image(volume.upload_to_image(args.force, args.image_name, args.container_format, @@ -732,7 +728,7 @@ def do_upload_to_image(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to backup.') + help='Name or ID of the volume to backup.') @utils.arg('--container', metavar='', help='Optional Backup container name. (Default=None)', default=None) @@ -745,12 +741,13 @@ def do_upload_to_image(cs, args): @utils.service_type('volume') def do_backup_create(cs, args): """Creates a backup.""" - backup = cs.backups.create(args.volume, + volume = utils.find_volume(cs, args.volume) + backup = cs.backups.create(volume.id, args.container, args.display_name, args.display_description) - info = {"volume_id": args.volume} + info = {"volume_id": volume.id} info.update(backup._info) if 'links' in info: @@ -793,25 +790,29 @@ def do_backup_delete(cs, args): @utils.arg('backup', metavar='', help='ID of the backup to restore.') -@utils.arg('--volume-id', metavar='', - help='Optional ID of the volume to restore to.', +@utils.arg('--volume-id', metavar='', + help='Optional ID(or name) of the volume to restore to.', default=None) @utils.service_type('volume') def do_backup_restore(cs, args): """Restore a backup.""" - cs.restores.restore(args.backup, - args.volume_id) + if args.volume: + volume_id = utils.find_volume(cs, args.volume).id + else: + volume_id = None + cs.restores.restore(args.backup, volume_id) @utils.arg('volume', metavar='', - help='ID of the volume to transfer.') + help='Name or 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, + volume = utils.find_volume(cs, args.volume) + transfer = cs.transfers.create(volume.id, args.display_name) info = dict() info.update(transfer._info) @@ -880,7 +881,7 @@ def do_transfer_show(cs, args): @utils.service_type('volume') def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) cs.volumes.extend(volume, args.new_size) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index ff17a5e..7366459 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -58,11 +58,6 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, time.sleep(poll_period) -def _find_volume(cs, volume): - """Get a volume by name or ID.""" - return utils.find_resource(cs.volumes, volume) - - def _find_volume_snapshot(cs, snapshot): """Get a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) @@ -185,7 +180,7 @@ def do_list(cs, args): def do_show(cs, args): """Show details about a volume.""" info = dict() - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) info.update(volume._info) info.pop('links', None) @@ -308,7 +303,7 @@ def do_create(cs, args): @utils.service_type('volumev2') def do_delete(cs, args): """Remove a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.delete() @@ -318,7 +313,7 @@ def do_delete(cs, args): @utils.service_type('volumev2') def do_force_delete(cs, args): """Attempt forced removal of a volume, regardless of its state.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.force_delete() @@ -331,7 +326,7 @@ def do_force_delete(cs, args): @utils.service_type('volumev2') def do_reset_state(cs, args): """Explicitly update the state of a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.reset_state(args.state) @@ -361,7 +356,7 @@ def do_rename(cs, args): elif args.description is not None: kwargs['description'] = args.description - _find_volume(cs, args.volume).update(**kwargs) + utils.find_volume(cs, args.volume).update(**kwargs) @utils.arg('volume', @@ -380,7 +375,7 @@ def do_rename(cs, args): @utils.service_type('volumev2') def do_metadata(cs, args): """Set or Delete metadata on a volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) metadata = _extract_metadata(args) if args.action == 'set': @@ -451,9 +446,9 @@ def do_snapshot_show(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('volume-id', - metavar='', - help='ID of the volume to snapshot') +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to snapshot') @utils.arg('--force', metavar='', help='Optional flag to indicate whether ' @@ -485,7 +480,8 @@ def do_snapshot_create(cs, args): if args.display_description is not None: args.description = args.display_description - snapshot = cs.volume_snapshots.create(args.volume_id, + volume = utils.find_volume(cs, args.volume) + snapshot = cs.volume_snapshots.create(volume.id, args.force, args.name, args.description) @@ -795,7 +791,7 @@ def _find_volume_type(cs, vtype): @utils.service_type('volumev2') def do_upload_to_image(cs, args): """Upload volume to image service as image.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) _print_volume_image(volume.upload_to_image(args.force, args.image_name, args.container_format, @@ -803,7 +799,7 @@ def do_upload_to_image(cs, args): @utils.arg('volume', metavar='', - help='ID of the volume to backup.') + help='Name or ID of the volume to backup.') @utils.arg('--container', metavar='', help='Optional backup container name. (Default=None)', default=None) @@ -827,12 +823,13 @@ def do_backup_create(cs, args): if args.display_description is not None: args.description = args.display_description - backup = cs.backups.create(args.volume, + volume = utils.find_volume(cs, args.volume) + backup = cs.backups.create(volume.id, args.container, args.name, args.description) - info = {"volume_id": args.volume} + info = {"volume_id": volume.id} info.update(backup._info) if 'links' in info: @@ -873,18 +870,21 @@ def do_backup_delete(cs, args): @utils.arg('backup', metavar='', help='ID of the backup to restore.') -@utils.arg('--volume-id', metavar='', - help='Optional ID of the volume to restore to.', +@utils.arg('--volume-id', metavar='', + help='Optional ID(or name) of the volume to restore to.', default=None) @utils.service_type('volumev2') def do_backup_restore(cs, args): """Restore a backup.""" - cs.restores.restore(args.backup, - args.volume_id) + if args.volume: + volume_id = utils.find_volume(cs, args.volume).id + else: + volume_id = None + cs.restores.restore(args.backup, volume_id) @utils.arg('volume', metavar='', - help='ID of the volume to transfer.') + help='Name or ID of the volume to transfer.') @utils.arg('--name', metavar='', default=None, @@ -897,7 +897,8 @@ def do_transfer_create(cs, args): if args.display_name is not None: args.name = args.display_name - transfer = cs.transfers.create(args.volume, + volume = utils.find_volume(cs, args.volume) + transfer = cs.transfers.create(volume.id, args.name) info = dict() info.update(transfer._info) @@ -960,7 +961,7 @@ def do_transfer_show(cs, args): @utils.service_type('volumev2') def do_extend(cs, args): """Attempt to extend the size of an existing volume.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) cs.volumes.extend(volume, args.new_size) From 327b397f6340c20d5f9e1fe3e80224aa0f5ae7fa Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Wed, 25 Sep 2013 10:30:17 +0300 Subject: [PATCH 095/165] Use v2 endpoint with v2 shell for migration Change the volume migration shell to use the v2 endpoint for v2. Change-Id: I0a902aa5e3f86c0d9562eb75677b11a364f9d371 Closes-Bug: #1230124 --- 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 a1f9ad3..337519a 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -809,7 +809,7 @@ def do_upload_to_image(cs, args): 'host-based migration mechanism, bypassing driver ' 'optimizations (Default=False).', default=False) -@utils.service_type('volume') +@utils.service_type('volumev2') def do_migrate(cs, args): """Migrate the volume to the new host.""" volume = _find_volume(cs, args.volume) From 876bff6398527a0686a1bc31f8ec7d45e3624a9a Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Thu, 8 Aug 2013 12:02:24 -0400 Subject: [PATCH 096/165] Error if arguments are not supplied for rename commands This changes behavior from: $ cinder rename volume1 $ to $ cinder rename volume1 ERROR: Must supply either display-name or display-description. for both 'rename' and 'snapshot-rename'. Change-Id: I675a3b1428a7fe10653394c80e4a5a473e14c740 --- cinderclient/tests/v1/test_shell.py | 16 ++++++++-------- cinderclient/tests/v2/test_shell.py | 16 ++++++++-------- cinderclient/v1/shell.py | 10 ++++++++++ cinderclient/v2/shell.py | 8 ++++++++ 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 014df94..13c2a19 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -127,7 +127,7 @@ class ShellTest(utils.TestCase): 'status=available&volume_id=1234') def test_rename(self): - # basic rename with positional agruments + # basic rename with positional arguments self.run_command('rename 1234 new-name') expected = {'volume': {'display_name': 'new-name'}} self.assert_called('PUT', '/volumes/1234', body=expected) @@ -143,12 +143,12 @@ class ShellTest(utils.TestCase): 'display_description': 'new-description', }} self.assert_called('PUT', '/volumes/1234', body=expected) - # noop, the only all will be the lookup - self.run_command('rename 1234') - self.assert_called('GET', '/volumes/1234') + + # Call rename with no arguments + self.assertRaises(SystemExit, self.run_command, 'rename') def test_rename_snapshot(self): - # basic rename with positional agruments + # basic rename with positional arguments self.run_command('snapshot-rename 1234 new-name') expected = {'snapshot': {'display_name': 'new-name'}} self.assert_called('PUT', '/snapshots/1234', body=expected) @@ -165,9 +165,9 @@ class ShellTest(utils.TestCase): 'display_description': 'new-description', }} self.assert_called('PUT', '/snapshots/1234', body=expected) - # noop, the only all will be the lookup - self.run_command('snapshot-rename 1234') - self.assert_called('GET', '/snapshots/1234') + + # Call snapshot-rename with no arguments + self.assertRaises(SystemExit, self.run_command, 'snapshot-rename') def test_set_metadata_set(self): self.run_command('metadata 1234 set key1=val1 key2=val2') diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 6f9a53f..4b7178a 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -105,7 +105,7 @@ class ShellTest(utils.TestCase): 'status=available&volume_id=1234') def test_rename(self): - # basic rename with positional agruments + # basic rename with positional arguments self.run_command('rename 1234 new-name') expected = {'volume': {'name': 'new-name'}} self.assert_called('PUT', '/volumes/1234', body=expected) @@ -121,12 +121,12 @@ class ShellTest(utils.TestCase): 'description': 'new-description', }} self.assert_called('PUT', '/volumes/1234', body=expected) - # noop, the only all will be the lookup - self.run_command('rename 1234') - self.assert_called('GET', '/volumes/1234') + + # Call rename with no arguments + self.assertRaises(SystemExit, self.run_command, 'rename') def test_rename_snapshot(self): - # basic rename with positional agruments + # basic rename with positional arguments self.run_command('snapshot-rename 1234 new-name') expected = {'snapshot': {'name': 'new-name'}} self.assert_called('PUT', '/snapshots/1234', body=expected) @@ -143,9 +143,9 @@ class ShellTest(utils.TestCase): 'description': 'new-description', }} self.assert_called('PUT', '/snapshots/1234', body=expected) - # noop, the only all will be the lookup - self.run_command('snapshot-rename 1234') - self.assert_called('GET', '/snapshots/1234') + + # Call snapshot-rename with no arguments + self.assertRaises(SystemExit, self.run_command, 'snapshot-rename') def test_set_metadata_set(self): self.run_command('metadata 1234 set key1=val1 key2=val2') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 34b5353..b0a223e 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -317,6 +317,11 @@ def do_rename(cs, args): kwargs['display_name'] = args.display_name if args.display_description is not None: kwargs['display_description'] = args.display_description + + if not any(kwargs): + msg = 'Must supply either display-name or display-description.' + raise exceptions.ClientException(code=1, message=msg) + utils.find_volume(cs, args.volume).update(**kwargs) @@ -461,6 +466,11 @@ def do_snapshot_rename(cs, args): kwargs['display_name'] = args.display_name if args.display_description is not None: kwargs['display_description'] = args.display_description + + if not any(kwargs): + msg = 'Must supply either display-name or display-description.' + raise exceptions.ClientException(code=1, message=msg) + _find_volume_snapshot(cs, args.snapshot).update(**kwargs) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index f1e45c4..bd4d54b 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -356,6 +356,10 @@ def do_rename(cs, args): elif args.description is not None: kwargs['description'] = args.description + if not any(kwargs): + msg = 'Must supply either name or description.' + raise exceptions.ClientException(code=1, message=msg) + utils.find_volume(cs, args.volume).update(**kwargs) @@ -522,6 +526,10 @@ def do_snapshot_rename(cs, args): elif args.display_description is not None: kwargs['description'] = args.display_description + if not any(kwargs): + msg = 'Must supply either name or description.' + raise exceptions.ClientException(code=1, message=msg) + _find_volume_snapshot(cs, args.snapshot).update(**kwargs) From 5ad95e9fd236a1f27cbcf1105494d6680a7d8ffe Mon Sep 17 00:00:00 2001 From: ZhiQiang Fan Date: Fri, 20 Sep 2013 03:31:42 +0800 Subject: [PATCH 097/165] Replace OpenStack LLC with OpenStack Foundation NOTE: * openstack/common/* should be synced from oslo, so i leave them untouched. * add (c) symbol for related lines, leave others untouched. Change-Id: I46a87c7f248d3468b1fdf5661411962faf2fb875 Fixes-Bug: #1214176 --- cinderclient/__init__.py | 2 +- cinderclient/base.py | 2 +- cinderclient/client.py | 2 +- cinderclient/extension.py | 2 +- cinderclient/service_catalog.py | 2 +- cinderclient/shell.py | 2 +- cinderclient/tests/v1/fakes.py | 2 +- cinderclient/tests/v1/test_quota_classes.py | 2 +- cinderclient/tests/v1/test_quotas.py | 2 +- cinderclient/tests/v1/test_services.py | 2 +- cinderclient/tests/v1/test_shell.py | 2 +- cinderclient/tests/v2/__init__.py | 2 +- cinderclient/tests/v2/contrib/test_list_extensions.py | 2 +- cinderclient/tests/v2/fakes.py | 2 +- cinderclient/tests/v2/test_auth.py | 2 +- cinderclient/tests/v2/test_quota_classes.py | 2 +- cinderclient/tests/v2/test_quotas.py | 2 +- cinderclient/tests/v2/test_services.py | 2 +- cinderclient/tests/v2/test_shell.py | 2 +- cinderclient/tests/v2/test_types.py | 2 +- cinderclient/tests/v2/test_volumes.py | 2 +- cinderclient/utils.py | 2 +- cinderclient/v1/__init__.py | 2 +- cinderclient/v1/client.py | 2 +- cinderclient/v1/contrib/__init__.py | 2 +- cinderclient/v1/contrib/list_extensions.py | 2 +- cinderclient/v1/quota_classes.py | 2 +- cinderclient/v1/quotas.py | 2 +- cinderclient/v1/services.py | 2 +- cinderclient/v1/shell.py | 2 +- cinderclient/v2/__init__.py | 2 +- cinderclient/v2/client.py | 2 +- cinderclient/v2/contrib/__init__.py | 2 +- cinderclient/v2/contrib/list_extensions.py | 2 +- cinderclient/v2/quota_classes.py | 2 +- cinderclient/v2/quotas.py | 2 +- cinderclient/v2/services.py | 2 +- cinderclient/v2/shell.py | 2 +- cinderclient/v2/volume_snapshots.py | 2 +- cinderclient/v2/volume_types.py | 2 +- cinderclient/v2/volumes.py | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/cinderclient/__init__.py b/cinderclient/__init__.py index 5d43513..bfaa627 100644 --- a/cinderclient/__init__.py +++ b/cinderclient/__init__.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright (c) 2012 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 diff --git a/cinderclient/base.py b/cinderclient/base.py index 4e29078..e1f44eb 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/client.py b/cinderclient/client.py index 0bb7b49..4846e4a 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. diff --git a/cinderclient/extension.py b/cinderclient/extension.py index 07d8450..84c67e9 100644 --- a/cinderclient/extension.py +++ b/cinderclient/extension.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/service_catalog.py b/cinderclient/service_catalog.py index 09ef59c..b43eaed 100644 --- a/cinderclient/service_catalog.py +++ b/cinderclient/service_catalog.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # Copyright 2011, Piston Cloud Computing, Inc. # # All Rights Reserved. diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 049324c..c9c1529 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -1,5 +1,5 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 40ddd4c..2c052c6 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -1,5 +1,5 @@ # Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack, LLC +# Copyright (c) 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. diff --git a/cinderclient/tests/v1/test_quota_classes.py b/cinderclient/tests/v1/test_quota_classes.py index 83e297f..0cb3122 100644 --- a/cinderclient/tests/v1/test_quota_classes.py +++ b/cinderclient/tests/v1/test_quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v1/test_quotas.py b/cinderclient/tests/v1/test_quotas.py index 7ebb061..faff9f6 100644 --- a/cinderclient/tests/v1/test_quotas.py +++ b/cinderclient/tests/v1/test_quotas.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v1/test_services.py b/cinderclient/tests/v1/test_services.py index 2320a26..7a1ec85 100644 --- a/cinderclient/tests/v1/test_services.py +++ b/cinderclient/tests/v1/test_services.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 014df94..ec44427 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v2/__init__.py b/cinderclient/tests/v2/__init__.py index 0cd9c14..f2c41f4 100644 --- a/cinderclient/tests/v2/__init__.py +++ b/cinderclient/tests/v2/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/tests/v2/contrib/test_list_extensions.py b/cinderclient/tests/v2/contrib/test_list_extensions.py index ff59cd2..66126be 100644 --- a/cinderclient/tests/v2/contrib/test_list_extensions.py +++ b/cinderclient/tests/v2/contrib/test_list_extensions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 474d10c..88a94bb 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack, LLC +# Copyright (c) 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. diff --git a/cinderclient/tests/v2/test_auth.py b/cinderclient/tests/v2/test_auth.py index 2ae3eed..9704840 100644 --- a/cinderclient/tests/v2/test_auth.py +++ b/cinderclient/tests/v2/test_auth.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/tests/v2/test_quota_classes.py b/cinderclient/tests/v2/test_quota_classes.py index 83cc710..0fee1e8 100644 --- a/cinderclient/tests/v2/test_quota_classes.py +++ b/cinderclient/tests/v2/test_quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v2/test_quotas.py b/cinderclient/tests/v2/test_quotas.py index 37ceeed..eb4531a 100644 --- a/cinderclient/tests/v2/test_quotas.py +++ b/cinderclient/tests/v2/test_quotas.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v2/test_services.py b/cinderclient/tests/v2/test_services.py index e4bce29..5ee3ea1 100644 --- a/cinderclient/tests/v2/test_services.py +++ b/cinderclient/tests/v2/test_services.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 6f9a53f..93940a7 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/tests/v2/test_types.py b/cinderclient/tests/v2/test_types.py index de8c743..70fbaeb 100644 --- a/cinderclient/tests/v2/test_types.py +++ b/cinderclient/tests/v2/test_types.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 73ee4a8..7b85369 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/utils.py b/cinderclient/utils.py index c7e4ebc..0fdcce9 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/__init__.py b/cinderclient/v1/__init__.py index fbb7b00..3637ffd 100644 --- a/cinderclient/v1/__init__.py +++ b/cinderclient/v1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012 OpenStack, LLC. +# Copyright (c) 2012 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 60376ab..4cbe6d0 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/contrib/__init__.py b/cinderclient/v1/contrib/__init__.py index dc6c3a3..788cea1 100644 --- a/cinderclient/v1/contrib/__init__.py +++ b/cinderclient/v1/contrib/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/contrib/list_extensions.py b/cinderclient/v1/contrib/list_extensions.py index 91fa040..5aab82f 100644 --- a/cinderclient/v1/contrib/list_extensions.py +++ b/cinderclient/v1/contrib/list_extensions.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index c6a85f4..1f880fb 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright (c) 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index bf37462..a0028a9 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/services.py b/cinderclient/v1/services.py index b2427dd..6afd5c5 100644 --- a/cinderclient/v1/services.py +++ b/cinderclient/v1/services.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 34b5353..fbd76bd 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/__init__.py b/cinderclient/v2/__init__.py index d09fab5..75afdec 100644 --- a/cinderclient/v2/__init__.py +++ b/cinderclient/v2/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 2f73ed6..9b8cad9 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/contrib/__init__.py b/cinderclient/v2/contrib/__init__.py index 0cd9c14..f2c41f4 100644 --- a/cinderclient/v2/contrib/__init__.py +++ b/cinderclient/v2/contrib/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # diff --git a/cinderclient/v2/contrib/list_extensions.py b/cinderclient/v2/contrib/list_extensions.py index 9031a51..eab9435 100644 --- a/cinderclient/v2/contrib/list_extensions.py +++ b/cinderclient/v2/contrib/list_extensions.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index 2d46a6d..4e44914 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 5b19b07..0972210 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/services.py b/cinderclient/v2/services.py index b2427dd..6afd5c5 100644 --- a/cinderclient/v2/services.py +++ b/cinderclient/v2/services.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index f1e45c4..509b5a4 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index 7aa9097..4e16ba8 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/cinderclient/v2/volume_types.py b/cinderclient/v2/volume_types.py index 9d4c2ff..bc382bd 100644 --- a/cinderclient/v2/volume_types.py +++ b/cinderclient/v2/volume_types.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 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. diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 2ec885d..e948cff 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may From 7b93e66bf0b652b7bc51e329663a31adbd54f7b0 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Sun, 29 Sep 2013 12:16:19 +0300 Subject: [PATCH 098/165] Fix find volume for migrate command Recently _find_volume was removed, but not fixed for migrate. Change-Id: I72b1b169bc67f89de10b7e729fc461b9114d3789 Closes-Bug: #1231117 --- cinderclient/tests/v1/test_shell.py | 6 ++++++ cinderclient/tests/v2/test_shell.py | 6 ++++++ cinderclient/v1/shell.py | 4 +++- cinderclient/v2/shell.py | 3 ++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 014df94..82cdfae 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -260,3 +260,9 @@ class ShellTest(utils.TestCase): Test encryption-type-delete shell command. """ self.skipTest("Not implemented") + + def test_migrate_volume(self): + self.run_command('migrate 1234 fakehost --force-host-copy=True') + expected = {'os-migrate_volume': {'force_host_copy': 'True', + 'host': 'fakehost'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 6f9a53f..bb50206 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -238,3 +238,9 @@ class ShellTest(utils.TestCase): Test encryption-type-delete shell command. """ self.skipTest("Not implemented") + + def test_migrate_volume(self): + self.run_command('migrate 1234 fakehost --force-host-copy=True') + expected = {'os-migrate_volume': {'force_host_copy': 'True', + 'host': 'fakehost'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 34b5353..0cb3a71 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1059,6 +1059,7 @@ def do_encryption_type_create(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to migrate') @utils.arg('host', metavar='', help='Destination host') @utils.arg('--force-host-copy', metavar='', + choices=['True', 'False'], required=False, help='Optional flag to force the use of the generic ' 'host-based migration mechanism, bypassing driver ' 'optimizations (Default=False).', @@ -1066,5 +1067,6 @@ def do_encryption_type_create(cs, args): @utils.service_type('volume') def do_migrate(cs, args): """Migrate the volume to the new host.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) + volume.migrate_volume(args.host, args.force_host_copy) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index f1e45c4..bef4964 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -801,6 +801,7 @@ def do_upload_to_image(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to migrate') @utils.arg('host', metavar='', help='Destination host') @utils.arg('--force-host-copy', metavar='', + choices=['True', 'False'], required=False, help='Optional flag to force the use of the generic ' 'host-based migration mechanism, bypassing driver ' 'optimizations (Default=False).', @@ -808,7 +809,7 @@ def do_upload_to_image(cs, args): @utils.service_type('volume') def do_migrate(cs, args): """Migrate the volume to the new host.""" - volume = _find_volume(cs, args.volume) + volume = utils.find_volume(cs, args.volume) volume.migrate_volume(args.host, args.force_host_copy) From a48821a29eac3d8fd633ee7d5c94594f16b29370 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Tue, 1 Oct 2013 15:19:38 +0800 Subject: [PATCH 099/165] Added --slowest --testr-args='--subunit ' when calling testr --- debian/changelog | 6 ++++++ debian/rules | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 8ebbcec..3e11396 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.5-3) UNRELEASED; urgency=low + + * Added --slowest --testr-args='--subunit ' when calling testr. + + -- Thomas Goirand Tue, 01 Oct 2013 15:19:25 +0800 + python-cinderclient (1:1.0.5-2) unstable; urgency=low * Added missing python-requests (build-)depends. diff --git a/debian/rules b/debian/rules index 9c1bdef..aa1822a 100755 --- a/debian/rules +++ b/debian/rules @@ -27,5 +27,5 @@ override_dh_install: ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) override_dh_auto_test: - python setup.py testr + python setup.py testr --slowest --testr-args='--subunit ' endif From 87628cc4852ba49e4dd300091b4e5494d4507714 Mon Sep 17 00:00:00 2001 From: Zhiteng Huang Date: Thu, 5 Sep 2013 12:02:59 +0800 Subject: [PATCH 100/165] Implement qos support This patch enables Cinder v1/v2 QoS API support, adding following subcommands to cinder client: * create QoS Specs cinder qos-create [ ...] * delete QoS Specs cinder qos-delete [--force ] 'force' is a flag indicates whether to delete a 'in-use' qos specs, which is still associated with other entities (e.g. volume types). * update QoS Specs - add new key/value pairs or update existing key/value cinder qos-key set key=value [key=value ...] - delete key/value pairs cinder qos-key unset key [key ...] * associate QoS Specs with specified volume type cinder qos-associate * disassociate QoS Specs from specified volume type cinder qos-disassociate * disassociate QoS Specs from all associated volume types cinder qos-disassociate-all * query entities that are associated with specified QoS Specs cinder qos-get-associations * list all QoS Specs cinder qos-list DocImpact Change-Id: Ie1ddd29fede8660b475bac14c4fc35496d5fe0bc --- cinderclient/tests/v1/fakes.py | 90 ++++++++++++++++++ cinderclient/tests/v1/test_qos.py | 79 ++++++++++++++++ cinderclient/tests/v2/fakes.py | 91 ++++++++++++++++++ cinderclient/tests/v2/test_qos.py | 79 ++++++++++++++++ cinderclient/v1/client.py | 2 + cinderclient/v1/qos_specs.py | 149 ++++++++++++++++++++++++++++++ cinderclient/v1/shell.py | 128 +++++++++++++++++++++++++ cinderclient/v2/client.py | 2 + cinderclient/v2/qos_specs.py | 149 ++++++++++++++++++++++++++++++ cinderclient/v2/shell.py | 134 ++++++++++++++++++++++++++- 10 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 cinderclient/tests/v1/test_qos.py create mode 100644 cinderclient/tests/v2/test_qos.py create mode 100644 cinderclient/v1/qos_specs.py create mode 100644 cinderclient/v2/qos_specs.py diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 2c052c6..87c9b39 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -116,6 +116,34 @@ def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} +def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None): + if not name: + name = 'fake-name' + if not specs: + specs = {} + + return { + 'qos_specs': { + 'id': id, + 'name': name, + 'consumer': 'back-end', + 'specs': specs, + }, + 'links': { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + } + + +def _stub_qos_associates(id, name): + return { + 'assoications_type': 'volume_type', + 'name': name, + 'id': id, + } + + def _stub_transfer_full(id, base_uri, tenant_id): return { 'id': id, @@ -505,6 +533,68 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + # + # QoSSpecs + # + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + return (200, {}, + _stub_qos_full(qos_id1, base_uri, tenant_id)) + + def get_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B' + return (200, {}, + {'qos_specs': [ + _stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'), + _stub_qos_full(qos_id2, base_uri, tenant_id)]}) + + def post_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_name = 'qos-name' + return (202, {}, + _stub_qos_full(qos_id, base_uri, tenant_id, qos_name)) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys( + self, **kw): + return (202, {}, None) + + def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations( + self, **kw): + type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF' + type_name1 = 'type1' + type_name2 = 'type2' + return (202, {}, + {'qos_associations': [ + _stub_qos_associates(type_id1, type_name1), + _stub_qos_associates(type_id2, type_name2)]}) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all( + self, **kw): + return (202, {}, None) + # # VolumeTransfers # diff --git a/cinderclient/tests/v1/test_qos.py b/cinderclient/tests/v1/test_qos.py new file mode 100644 index 0000000..e127a47 --- /dev/null +++ b/cinderclient/tests/v1/test_qos.py @@ -0,0 +1,79 @@ +# Copyright (C) 2013 eBay Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class QoSSpecsTest(utils.TestCase): + + def test_create(self): + specs = dict(k1='v1', k2='v2') + cs.qos_specs.create('qos-name', specs) + cs.assert_called('POST', '/qos-specs') + + def test_get(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get(qos_id) + cs.assert_called('GET', '/qos-specs/%s' % qos_id) + + def test_list(self): + cs.qos_specs.list() + cs.assert_called('GET', '/qos-specs') + + def test_delete(self): + cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C') + cs.assert_called('DELETE', + '/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?' + 'force=False') + + def test_set_keys(self): + body = {'qos_specs': dict(k1='v1')} + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.set_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s' % qos_id) + + def test_unset_keys(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + body = {'keys': ['k1']} + cs.qos_specs.unset_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id) + + def test_get_associations(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get_associations(qos_id) + cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id) + + def test_associate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.associate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.disassociate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate_all(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.disassociate_all(qos_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 88a94bb..d057456 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -119,6 +119,34 @@ def _stub_backup(id, base_uri, tenant_id): } +def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None): + if not name: + name = 'fake-name' + if not specs: + specs = {} + + return { + 'qos_specs': { + 'id': id, + 'name': name, + 'consumer': 'back-end', + 'specs': specs, + }, + 'links': { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + } + + +def _stub_qos_associates(id, name): + return { + 'assoications_type': 'volume_type', + 'name': name, + 'id': id, + } + + def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} @@ -512,6 +540,69 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + # + # QoSSpecs + # + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + return (200, {}, + _stub_qos_full(qos_id1, base_uri, tenant_id)) + + def get_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B' + return (200, {}, + {'qos_specs': [ + _stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'), + _stub_qos_full(qos_id2, base_uri, tenant_id)]}) + + def post_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_name = 'qos-name' + return (202, {}, + _stub_qos_full(qos_id, base_uri, tenant_id, qos_name)) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys( + self, **kw): + return (202, {}, None) + + def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations( + self, **kw): + type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF' + type_name1 = 'type1' + type_name2 = 'type2' + return (202, {}, + {'qos_associations': [ + _stub_qos_associates(type_id1, type_name1), + _stub_qos_associates(type_id2, type_name2)]}) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all( + self, **kw): + return (202, {}, None) + + # # # VolumeTransfers # diff --git a/cinderclient/tests/v2/test_qos.py b/cinderclient/tests/v2/test_qos.py new file mode 100644 index 0000000..3f3e6cf --- /dev/null +++ b/cinderclient/tests/v2/test_qos.py @@ -0,0 +1,79 @@ +# Copyright (C) 2013 eBay Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinderclient.tests import utils +from cinderclient.tests.v2 import fakes + + +cs = fakes.FakeClient() + + +class QoSSpecsTest(utils.TestCase): + + def test_create(self): + specs = dict(k1='v1', k2='v2') + cs.qos_specs.create('qos-name', specs) + cs.assert_called('POST', '/qos-specs') + + def test_get(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get(qos_id) + cs.assert_called('GET', '/qos-specs/%s' % qos_id) + + def test_list(self): + cs.qos_specs.list() + cs.assert_called('GET', '/qos-specs') + + def test_delete(self): + cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C') + cs.assert_called('DELETE', + '/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?' + 'force=False') + + def test_set_keys(self): + body = {'qos_specs': dict(k1='v1')} + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.set_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s' % qos_id) + + def test_unset_keys(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + body = {'keys': ['k1']} + cs.qos_specs.unset_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id) + + def test_get_associations(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get_associations(qos_id) + cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id) + + def test_associate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.associate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.disassociate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate_all(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.disassociate_all(qos_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id) diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 4cbe6d0..82d1ee0 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -16,6 +16,7 @@ from cinderclient import client from cinderclient.v1 import availability_zones from cinderclient.v1 import limits +from cinderclient.v1 import qos_specs from cinderclient.v1 import quota_classes from cinderclient.v1 import quotas from cinderclient.v1 import services @@ -62,6 +63,7 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) + self.qos_specs = qos_specs.QoSSpecsManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v1/qos_specs.py b/cinderclient/v1/qos_specs.py new file mode 100644 index 0000000..b4e4272 --- /dev/null +++ b/cinderclient/v1/qos_specs.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013 eBay Inc. +# Copyright (c) OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +QoS Specs interface. +""" + +from cinderclient import base + + +class QoSSpecs(base.Resource): + """QoS specs entity represents quality-of-service parameters/requirements. + + A QoS specs is a set of parameters or requirements for quality-of-service + purpose, which can be associated with volume types (for now). In future, + QoS specs may be extended to be associated other entities, such as single + volume. + """ + def __repr__(self): + return "" % self.name + + def delete(self): + return self.manager.delete(self) + + +class QoSSpecsManager(base.ManagerWithFind): + """ + Manage :class:`QoSSpecs` resources. + """ + resource_class = QoSSpecs + + def list(self): + """Get a list of all qos specs. + + :rtype: list of :class:`QoSSpecs`. + """ + return self._list("/qos-specs", "qos_specs") + + def get(self, qos_specs): + """Get a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to get. + :rtype: :class:`QoSSpecs` + """ + return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") + + def delete(self, qos_specs, force=False): + """Delete a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. + :param force: Flag that indicates whether to delete target qos specs + if it was in-use. + """ + self._delete("/qos-specs/%s?force=%s" % + (base.getid(qos_specs), force)) + + def create(self, name, specs): + """Create a qos specs. + + :param name: Descriptive name of the qos specs, must be unique + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": { + "name": name, + } + } + + body["qos_specs"].update(specs) + return self._create("/qos-specs", body, "qos_specs") + + def set_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": {} + } + + body["qos_specs"].update(specs) + return self._update("/qos-specs/%s" % qos_specs, body) + + def unset_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A list of key to be unset + :rtype: :class:`QoSSpecs` + """ + + body = {'keys': specs} + + return self._update("/qos-specs/%s/delete_keys" % qos_specs, + body) + + def get_associations(self, qos_specs): + """Get associated entities of a qos specs. + + :param qos_specs: The id of the :class: `QoSSpecs` + :return: a list of entities that associated with specific qos specs. + """ + return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), + "qos_associations") + + def associate(self, qos_specs, vol_type_id): + """Associate a volume type with specific qos specs. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate(self, qos_specs, vol_type_id): + """Disassociate qos specs from volume type. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate_all(self, qos_specs): + """Disassociate all entities from specific qos specs. + + :param qos_specs: The qos specs to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate_all" % + base.getid(qos_specs)) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index fbd76bd..afb5f85 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -24,6 +24,7 @@ import sys import time from cinderclient import exceptions +from cinderclient.openstack.common import strutils from cinderclient import utils from cinderclient.v1 import availability_zones @@ -75,6 +76,11 @@ def _find_transfer(cs, transfer): return utils.find_resource(cs.transfers, transfer) +def _find_qos_specs(cs, qos_specs): + """Get a qos specs by ID.""" + return utils.find_resource(cs.qos_specs, qos_specs) + + def _print_volume(volume): utils.print_dict(volume._info) @@ -1068,3 +1074,125 @@ def do_migrate(cs, args): """Migrate the volume to the new host.""" volume = _find_volume(cs, args.volume) volume.migrate_volume(args.host, args.force_host_copy) + + +def _print_qos_specs(qos_specs): + utils.print_dict(qos_specs._info) + + +def _print_qos_specs_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_qos_specs_and_associations_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_associations_list(associations): + utils.print_list(associations, ['Association_Type', 'Name', 'ID']) + + +@utils.arg('name', + metavar='', + help="Name of the new QoS specs") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Specifications for QoS') +@utils.service_type('volume') +def do_qos_create(cs, args): + """Create a new qos specs.""" + keypair = None + if args.metadata is not None: + keypair = _extract_metadata(args) + qos_specs = cs.qos_specs.create(args.name, keypair) + _print_qos_specs(qos_specs) + + +@utils.service_type('volume') +def do_qos_list(cs, args): + """Get full list of qos specs.""" + qos_specs = cs.qos_specs.list() + _print_qos_specs_list(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to show.') +@utils.service_type('volume') +def do_qos_show(cs, args): + """Get a specific qos specs.""" + qos_specs = _find_qos_specs(cs, args.qos_specs) + _print_qos_specs(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to delete.') +@utils.arg('--force', + metavar='', + default=False, + help='Optional flag that indicates whether to delete ' + 'specified qos specs even if it is in-use.') +@utils.service_type('volume') +def do_qos_delete(cs, args): + """Delete a specific qos specs.""" + force = strutils.bool_from_string(args.force) + qos_specs = _find_qos_specs(cs, args.qos_specs) + cs.qos_specs.delete(qos_specs, force) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volume') +def do_qos_associate(cs, args): + """Associate qos specs with specific volume type.""" + cs.qos_specs.associate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volume') +def do_qos_disassociate(cs, args): + """Disassociate qos specs from specific volume type.""" + cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs to be operate on.') +@utils.service_type('volume') +def do_qos_disassociate_all(cs, args): + """Disassociate qos specs from all of its associations.""" + cs.qos_specs.disassociate_all(args.qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos specs') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', metavar='key=value', + nargs='+', + default=[], + help='QoS specs to set/unset (only key is necessary on unset)') +def do_qos_key(cs, args): + """Set or unset specifications for a qos spec.""" + keypair = _extract_metadata(args) + + if args.action == 'set': + cs.qos_specs.set_keys(args.qos_specs, keypair) + elif args.action == 'unset': + cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs.') +@utils.service_type('volume') +def do_qos_get_association(cs, args): + """Get all associations of specific qos specs.""" + associations = cs.qos_specs.get_associations(args.qos_specs) + _print_associations_list(associations) diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 9b8cad9..7b91c23 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -16,6 +16,7 @@ from cinderclient import client from cinderclient.v1 import availability_zones from cinderclient.v2 import limits +from cinderclient.v2 import qos_specs from cinderclient.v2 import quota_classes from cinderclient.v2 import quotas from cinderclient.v2 import services @@ -60,6 +61,7 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) + self.qos_specs = qos_specs.QoSSpecsManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v2/qos_specs.py b/cinderclient/v2/qos_specs.py new file mode 100644 index 0000000..b4e4272 --- /dev/null +++ b/cinderclient/v2/qos_specs.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013 eBay Inc. +# Copyright (c) OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +QoS Specs interface. +""" + +from cinderclient import base + + +class QoSSpecs(base.Resource): + """QoS specs entity represents quality-of-service parameters/requirements. + + A QoS specs is a set of parameters or requirements for quality-of-service + purpose, which can be associated with volume types (for now). In future, + QoS specs may be extended to be associated other entities, such as single + volume. + """ + def __repr__(self): + return "" % self.name + + def delete(self): + return self.manager.delete(self) + + +class QoSSpecsManager(base.ManagerWithFind): + """ + Manage :class:`QoSSpecs` resources. + """ + resource_class = QoSSpecs + + def list(self): + """Get a list of all qos specs. + + :rtype: list of :class:`QoSSpecs`. + """ + return self._list("/qos-specs", "qos_specs") + + def get(self, qos_specs): + """Get a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to get. + :rtype: :class:`QoSSpecs` + """ + return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") + + def delete(self, qos_specs, force=False): + """Delete a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. + :param force: Flag that indicates whether to delete target qos specs + if it was in-use. + """ + self._delete("/qos-specs/%s?force=%s" % + (base.getid(qos_specs), force)) + + def create(self, name, specs): + """Create a qos specs. + + :param name: Descriptive name of the qos specs, must be unique + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": { + "name": name, + } + } + + body["qos_specs"].update(specs) + return self._create("/qos-specs", body, "qos_specs") + + def set_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": {} + } + + body["qos_specs"].update(specs) + return self._update("/qos-specs/%s" % qos_specs, body) + + def unset_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A list of key to be unset + :rtype: :class:`QoSSpecs` + """ + + body = {'keys': specs} + + return self._update("/qos-specs/%s/delete_keys" % qos_specs, + body) + + def get_associations(self, qos_specs): + """Get associated entities of a qos specs. + + :param qos_specs: The id of the :class: `QoSSpecs` + :return: a list of entities that associated with specific qos specs. + """ + return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), + "qos_associations") + + def associate(self, qos_specs, vol_type_id): + """Associate a volume type with specific qos specs. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate(self, qos_specs, vol_type_id): + """Disassociate qos specs from volume type. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate_all(self, qos_specs): + """Disassociate all entities from specific qos specs. + + :param qos_specs: The qos specs to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate_all" % + base.getid(qos_specs)) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 509b5a4..c1eb0c4 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -25,6 +25,7 @@ import six from cinderclient import exceptions from cinderclient import utils +from cinderclient.openstack.common import strutils from cinderclient.v2 import availability_zones @@ -73,6 +74,11 @@ def _find_transfer(cs, transfer): return utils.find_resource(cs.transfers, transfer) +def _find_qos_specs(cs, qos_specs): + """Get a qos specs by ID.""" + return utils.find_resource(cs.qos_specs, qos_specs) + + def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) @@ -106,7 +112,7 @@ def _translate_availability_zone_keys(collection): def _extract_metadata(args): metadata = {} - for metadatum in args.metadata[0]: + for metadatum in args.metadata: # unset doesn't require a val, so we have the if/else if '=' in metadatum: (key, value) = metadatum.split('=', 1) @@ -369,7 +375,6 @@ def do_rename(cs, args): @utils.arg('metadata', metavar='', nargs='+', - action='append', default=[], help='Metadata to set/unset (only key is necessary on unset)') @utils.service_type('volumev2') @@ -592,12 +597,11 @@ def do_type_delete(cs, args): @utils.arg('metadata', metavar='', nargs='+', - action='append', default=[], help='Extra_specs to set/unset (only key is necessary on unset)') @utils.service_type('volumev2') def do_type_key(cs, args): - "Set or unset extra_spec for a volume type.""" + """Set or unset extra_spec for a volume type.""" vtype = _find_volume_type(cs, args.vtype) keypair = _extract_metadata(args) @@ -1148,3 +1152,125 @@ def do_encryption_type_create(cs, args): result = cs.volume_encryption_types.create(volume_type, body) _print_volume_encryption_type_list([result]) + + +def _print_qos_specs(qos_specs): + utils.print_dict(qos_specs._info) + + +def _print_qos_specs_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_qos_specs_and_associations_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_associations_list(associations): + utils.print_list(associations, ['Association_Type', 'Name', 'ID']) + + +@utils.arg('name', + metavar='', + help="Name of the new QoS specs") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Specifications for QoS') +@utils.service_type('volumev2') +def do_qos_create(cs, args): + """Create a new qos specs.""" + keypair = None + if args.metadata is not None: + keypair = _extract_metadata(args) + qos_specs = cs.qos_specs.create(args.name, keypair) + _print_qos_specs(qos_specs) + + +@utils.service_type('volumev2') +def do_qos_list(cs, args): + """Get full list of qos specs.""" + qos_specs = cs.qos_specs.list() + _print_qos_specs_list(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to show.') +@utils.service_type('volumev2') +def do_qos_show(cs, args): + """Get a specific qos specs.""" + qos_specs = _find_qos_specs(cs, args.qos_specs) + _print_qos_specs(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to delete.') +@utils.arg('--force', + metavar='', + default=False, + help='Optional flag that indicates whether to delete ' + 'specified qos specs even if it is in-use.') +@utils.service_type('volumev2') +def do_qos_delete(cs, args): + """Delete a specific qos specs.""" + force = strutils.bool_from_string(args.force) + qos_specs = _find_qos_specs(cs, args.qos_specs) + cs.qos_specs.delete(qos_specs, force) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volumev2') +def do_qos_associate(cs, args): + """Associate qos specs with specific volume type.""" + cs.qos_specs.associate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volumev2') +def do_qos_disassociate(cs, args): + """Disassociate qos specs from specific volume type.""" + cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs to be operate on.') +@utils.service_type('volumev2') +def do_qos_disassociate_all(cs, args): + """Disassociate qos specs from all of its associations.""" + cs.qos_specs.disassociate_all(args.qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos specs') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', metavar='key=value', + nargs='+', + default=[], + help='QoS specs to set/unset (only key is necessary on unset)') +def do_qos_key(cs, args): + """Set or unset specifications for a qos spec.""" + keypair = _extract_metadata(args) + + if args.action == 'set': + cs.qos_specs.set_keys(args.qos_specs, keypair) + elif args.action == 'unset': + cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs.') +@utils.service_type('volumev2') +def do_qos_get_association(cs, args): + """Get all associations of specific qos specs.""" + associations = cs.qos_specs.get_associations(args.qos_specs) + _print_associations_list(associations) From 945b211cd0bab1636168d2227da67eb37c8505af Mon Sep 17 00:00:00 2001 From: John Griffith Date: Wed, 2 Oct 2013 14:50:50 -0600 Subject: [PATCH 101/165] Synch up with OSLO-Incubator Wanted to get updates before next push to pypi. The main thing driving this is we're now calling in some methods from strutils and gettextutils that don't have the py33 updates. Change-Id: I358f08f5c5c0a9ee6729947a8f01b1e96de0a729 --- .../openstack/common/apiclient/base.py | 2 +- .../openstack/common/apiclient/client.py | 2 +- cinderclient/openstack/common/gettextutils.py | 106 ++++++++++++++---- tools/install_venv_common.py | 20 ++-- 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/cinderclient/openstack/common/apiclient/base.py b/cinderclient/openstack/common/apiclient/base.py index 1b3e790..caef843 100644 --- a/cinderclient/openstack/common/apiclient/base.py +++ b/cinderclient/openstack/common/apiclient/base.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC +# Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. diff --git a/cinderclient/openstack/common/apiclient/client.py b/cinderclient/openstack/common/apiclient/client.py index 85837da..77d4579 100644 --- a/cinderclient/openstack/common/apiclient/client.py +++ b/cinderclient/openstack/common/apiclient/client.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC +# Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 Grid Dynamics diff --git a/cinderclient/openstack/common/gettextutils.py b/cinderclient/openstack/common/gettextutils.py index e887869..ce57f89 100644 --- a/cinderclient/openstack/common/gettextutils.py +++ b/cinderclient/openstack/common/gettextutils.py @@ -26,10 +26,13 @@ Usual usage in an openstack.common module: import copy import gettext -import logging.handlers +import logging import os import re -import UserString +try: + import UserString as _userString +except ImportError: + import collections as _userString from babel import localedata import six @@ -37,11 +40,29 @@ import six _localedir = os.environ.get('cinderclient'.upper() + '_LOCALEDIR') _t = gettext.translation('cinderclient', localedir=_localedir, fallback=True) -_AVAILABLE_LANGUAGES = [] +_AVAILABLE_LANGUAGES = {} +USE_LAZY = False + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + global USE_LAZY + USE_LAZY = True def _(msg): - return _t.ugettext(msg) + if USE_LAZY: + return Message(msg, 'cinderclient') + else: + if six.PY3: + return _t.gettext(msg) + return _t.ugettext(msg) def install(domain, lazy=False): @@ -86,24 +107,28 @@ def install(domain, lazy=False): """ return Message(msg, domain) - import __builtin__ - __builtin__.__dict__['_'] = _lazy_gettext + from six import moves + moves.builtins.__dict__['_'] = _lazy_gettext else: localedir = '%s_LOCALEDIR' % domain.upper() - gettext.install(domain, - localedir=os.environ.get(localedir), - unicode=True) + if six.PY3: + gettext.install(domain, + localedir=os.environ.get(localedir)) + else: + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) -class Message(UserString.UserString, object): +class Message(_userString.UserString, object): """Class used to encapsulate translatable messages.""" def __init__(self, msg, domain): # _msg is the gettext msgid and should never change self._msg = msg self._left_extra_msg = '' self._right_extra_msg = '' + self._locale = None self.params = None - self.locale = None self.domain = domain @property @@ -123,8 +148,13 @@ class Message(UserString.UserString, object): localedir=localedir, fallback=True) + if six.PY3: + ugettext = lang.gettext + else: + ugettext = lang.ugettext + full_msg = (self._left_extra_msg + - lang.ugettext(self._msg) + + ugettext(self._msg) + self._right_extra_msg) if self.params is not None: @@ -132,6 +162,33 @@ class Message(UserString.UserString, object): return six.text_type(full_msg) + @property + def locale(self): + return self._locale + + @locale.setter + def locale(self, value): + self._locale = value + if not self.params: + return + + # This Message object may have been constructed with one or more + # Message objects as substitution parameters, given as a single + # Message, or a tuple or Map containing some, so when setting the + # locale for this Message we need to set it for those Messages too. + if isinstance(self.params, Message): + self.params.locale = value + return + if isinstance(self.params, tuple): + for param in self.params: + if isinstance(param, Message): + param.locale = value + return + if isinstance(self.params, dict): + for param in self.params.values(): + if isinstance(param, Message): + param.locale = value + def _save_dictionary_parameter(self, dict_param): full_msg = self.data # look for %(blah) fields in string; @@ -150,7 +207,7 @@ class Message(UserString.UserString, object): params[key] = copy.deepcopy(dict_param[key]) except TypeError: # cast uncopyable thing to unicode string - params[key] = unicode(dict_param[key]) + params[key] = six.text_type(dict_param[key]) return params @@ -169,7 +226,7 @@ class Message(UserString.UserString, object): try: self.params = copy.deepcopy(other) except TypeError: - self.params = unicode(other) + self.params = six.text_type(other) return self @@ -178,11 +235,13 @@ class Message(UserString.UserString, object): return self.data def __str__(self): + if six.PY3: + return self.__unicode__() return self.data.encode('utf-8') def __getstate__(self): to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', - 'domain', 'params', 'locale'] + 'domain', 'params', '_locale'] new_dict = self.__dict__.fromkeys(to_copy) for attr in to_copy: new_dict[attr] = copy.deepcopy(self.__dict__[attr]) @@ -236,7 +295,7 @@ class Message(UserString.UserString, object): if name in ops: return getattr(self.data, name) else: - return UserString.UserString.__getattribute__(self, name) + return _userString.UserString.__getattribute__(self, name) def get_available_languages(domain): @@ -244,8 +303,8 @@ def get_available_languages(domain): :param domain: the domain to get languages for """ - if _AVAILABLE_LANGUAGES: - return _AVAILABLE_LANGUAGES + if domain in _AVAILABLE_LANGUAGES: + return copy.copy(_AVAILABLE_LANGUAGES[domain]) localedir = '%s_LOCALEDIR' % domain.upper() find = lambda x: gettext.find(domain, @@ -254,7 +313,7 @@ def get_available_languages(domain): # NOTE(mrodden): en_US should always be available (and first in case # order matters) since our in-line message strings are en_US - _AVAILABLE_LANGUAGES.append('en_US') + language_list = ['en_US'] # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove @@ -264,16 +323,17 @@ def get_available_languages(domain): locale_identifiers = list_identifiers() for i in locale_identifiers: if find(i) is not None: - _AVAILABLE_LANGUAGES.append(i) - return _AVAILABLE_LANGUAGES + language_list.append(i) + _AVAILABLE_LANGUAGES[domain] = language_list + return copy.copy(language_list) def get_localized_message(message, user_locale): """Gets a localized version of the given message in the given locale.""" - if (isinstance(message, Message)): + if isinstance(message, Message): if user_locale: message.locale = user_locale - return unicode(message) + return six.text_type(message) else: return message diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 6ce5d00..92d66ae 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -119,8 +119,7 @@ class InstallVenv(object): self.pip_install('setuptools') self.pip_install('pbr') - self.pip_install('-r', self.requirements) - self.pip_install('-r', self.test_requirements) + self.pip_install('-r', self.requirements, '-r', self.test_requirements) def post_process(self): self.get_distro().post_process() @@ -202,12 +201,13 @@ class Fedora(Distro): 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'.") + if os.path.exists('contrib/redhat-eventlet.patch'): + # 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') + # 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 6437166e1ea194f7466ad714245dd1e4a0403707 Mon Sep 17 00:00:00 2001 From: Seif Lotfy Date: Thu, 26 Sep 2013 21:48:17 +0000 Subject: [PATCH 102/165] Add quota-usage command Added a new command quota-usage to allow querying the usage of the quota for a given tenant Change-Id: I720e6df94f9fedbb8b6385bd1707600542aaea08 --- cinderclient/tests/v1/test_quotas.py | 2 +- cinderclient/tests/v2/test_quotas.py | 2 +- cinderclient/utils.py | 5 ++++- cinderclient/v1/quotas.py | 5 +++-- cinderclient/v1/shell.py | 26 ++++++++++++++++++++++++++ cinderclient/v2/quotas.py | 5 +++-- cinderclient/v2/shell.py | 26 ++++++++++++++++++++++++++ 7 files changed, 64 insertions(+), 7 deletions(-) diff --git a/cinderclient/tests/v1/test_quotas.py b/cinderclient/tests/v1/test_quotas.py index faff9f6..4751954 100644 --- a/cinderclient/tests/v1/test_quotas.py +++ b/cinderclient/tests/v1/test_quotas.py @@ -25,7 +25,7 @@ class QuotaSetsTest(utils.TestCase): def test_tenant_quotas_get(self): tenant_id = 'test' cs.quotas.get(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + cs.assert_called('GET', '/os-quota-sets/%s?usage=False' % tenant_id) def test_tenant_quotas_defaults(self): tenant_id = 'test' diff --git a/cinderclient/tests/v2/test_quotas.py b/cinderclient/tests/v2/test_quotas.py index eb4531a..5a61dbb 100644 --- a/cinderclient/tests/v2/test_quotas.py +++ b/cinderclient/tests/v2/test_quotas.py @@ -25,7 +25,7 @@ class QuotaSetsTest(utils.TestCase): def test_tenant_quotas_get(self): tenant_id = 'test' cs.quotas.get(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + cs.assert_called('GET', '/os-quota-sets/%s?usage=False' % tenant_id) def test_tenant_quotas_defaults(self): tenant_id = 'test' diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 0fdcce9..7a728d7 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -168,7 +168,10 @@ def print_list(objs, fields, formatters={}, order_by=None): field_name = field.replace(' ', '_') else: field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') + if type(o) == dict and field in o: + data = o[field] + else: + data = getattr(o, field_name, '') row.append(data) pt.add_row(row) diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py index a0028a9..adcd335 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -32,10 +32,11 @@ class QuotaSet(base.Resource): class QuotaSetManager(base.Manager): resource_class = QuotaSet - def get(self, tenant_id): + def get(self, tenant_id, usage=False): if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id - return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") + return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage), + "quota_set") def update(self, tenant_id, **updates): body = {'quota_set': {'tenant_id': tenant_id}} diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index fbd76bd..97cd3cf 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -562,6 +562,7 @@ def do_credentials(cs, args): _quota_resources = ['volumes', 'snapshots', 'gigabytes'] +_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit'] def _quota_show(quotas): @@ -577,6 +578,22 @@ def _quota_show(quotas): utils.print_dict(quota_dict) +def _quota_usage_show(quotas): + quota_list = [] + 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_info = getattr(quotas, resource, None) + quota_info['Type'] = resource + quota_info = dict((k.capitalize(), v) for k, v in quota_info.items()) + quota_list.append(quota_info) + utils.print_list(quota_list, _quota_infos) + + def _quota_update(manager, identifier, args): updates = {} for resource in _quota_resources: @@ -599,6 +616,15 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant)) +@utils.arg('tenant', metavar='', + help='UUID of tenant to list the quota usage for.') +@utils.service_type('volume') +def do_quota_usage(cs, args): + """List the quota usage for a tenant.""" + + _quota_usage_show(cs.quotas.get(args.tenant, usage=True)) + + @utils.arg('tenant', metavar='', help='UUID of tenant to list the default quotas for.') @utils.service_type('volume') diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index 0972210..59beb51 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -30,10 +30,11 @@ class QuotaSet(base.Resource): class QuotaSetManager(base.Manager): resource_class = QuotaSet - def get(self, tenant_id): + def get(self, tenant_id, usage=False): if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id - return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") + return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage), + "quota_set") def update(self, tenant_id, **updates): body = {'quota_set': {'tenant_id': tenant_id}} diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index ce48744..247883f 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -622,6 +622,7 @@ def do_credentials(cs, args): _quota_resources = ['volumes', 'snapshots', 'gigabytes'] +_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit'] def _quota_show(quotas): @@ -637,6 +638,22 @@ def _quota_show(quotas): utils.print_dict(quota_dict) +def _quota_usage_show(quotas): + quota_list = [] + 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_info = getattr(quotas, resource, None) + quota_info['Type'] = resource + quota_info = dict((k.capitalize(), v) for k, v in quota_info.items()) + quota_list.append(quota_info) + utils.print_list(quota_list, _quota_infos) + + def _quota_update(manager, identifier, args): updates = {} for resource in _quota_resources: @@ -660,6 +677,15 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant)) +@utils.arg('tenant', metavar='', + help='UUID of tenant to list the quota usage for.') +@utils.service_type('volumev2') +def do_quota_usage(cs, args): + """List the quota usage for a tenant.""" + + _quota_usage_show(cs.quotas.get(args.tenant, usage=True)) + + @utils.arg('tenant', metavar='', help='UUID of tenant to list the default quotas for.') From 4a507601d7dded2efb1bd2e885155ba38db9538c Mon Sep 17 00:00:00 2001 From: John Griffith Date: Thu, 3 Oct 2013 17:22:34 -0600 Subject: [PATCH 103/165] Update docs/index.rst with release info for 1.0.6 Add features and bugs list for 1.0.6 pypi release. Change-Id: I27134a04670bcfd1bca201ce1e0bba201e8ff98c --- doc/source/index.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 3a658b1..e72aed1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,6 +29,31 @@ See also :doc:`/man/cinder`. Release Notes ============= +1.0.6 +----- +* Add support for multiple endpoints +* Add response info for backup command +* Add metadata option to cinder list command +* Add timeout parameter for requests +* Add update action for snapshot metadata +* Add encryption metadata support +* Add volume migrate support + +.. _1221104: http://bugs.launchpad.net/python-cinderclient/+bug/1221104 +.. _1220590: http://bugs.launchpad.net/python-cinderclient/+bug/1220590 +.. _1220147: http://bugs.launchpad.net/python-cinderclient/+bug/1220147 +.. _1214176: http://bugs.launchpad.net/python-cinderclient/+bug/1214176 +.. _1210874: http://bugs.launchpad.net/python-cinderclient/+bug/1210874 +.. _1210296: http://bugs.launchpad.net/python-cinderclient/+bug/1210296 +.. _1210292: http://bugs.launchpad.net/python-cinderclient/+bug/1210292 +.. _1207635: http://bugs.launchpad.net/python-cinderclient/+bug/1207635 +.. _1207609: http://bugs.launchpad.net/python-cinderclient/+bug/1207609 +.. _1207260: http://bugs.launchpad.net/python-cinderclient/+bug/1207260 +.. _1206968: http://bugs.launchpad.net/python-cinderclient/+bug/1206968 +.. _1203471: http://bugs.launchpad.net/python-cinderclient/+bug/1203471 +.. _1200214: http://bugs.launchpad.net/python-cinderclient/+bug/1200214 +.. _1195014: http://bugs.launchpad.net/python-cinderclient/+bug/1195014 + 1.0.5 ----- * Add CLI man page From e78356300b4c29638b4fbaeacc2ef6a88939e091 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 7 Oct 2013 01:17:45 +0800 Subject: [PATCH 104/165] Now packaging upstream release 1.0.6 --- debian/changelog | 2 +- debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 3e11396..763d97b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python-cinderclient (1:1.0.5-3) UNRELEASED; urgency=low +python-cinderclient (1:1.0.6-1) unstable; urgency=low * Added --slowest --testr-args='--subunit ' when calling testr. diff --git a/debian/control b/debian/control index ef8e5ed..02c9bed 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ Uploaders: Julien Danjou , Thomas Goirand , Ghe Rivero , Mehdi Abaakouk -Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~) +Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~), python-pbr (>= 0.5.16) Build-Depends-Indep: openstack-pkg-tools, pep8, python-httplib2, From 5d4b5818c93245c33f22951f46b78b639de1989f Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 7 Oct 2013 01:23:42 +0800 Subject: [PATCH 105/165] Fixed new (build-)depends for this release. --- debian/changelog | 2 ++ debian/control | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/debian/changelog b/debian/changelog index 763d97b..ec9a172 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,7 @@ python-cinderclient (1:1.0.6-1) unstable; urgency=low + * New upstream release. + * Fixed new (build-)depends for this release. * Added --slowest --testr-args='--subunit ' when calling testr. -- Thomas Goirand Tue, 01 Oct 2013 15:19:25 +0800 diff --git a/debian/control b/debian/control index 02c9bed..2e99b5d 100644 --- a/debian/control +++ b/debian/control @@ -6,21 +6,25 @@ Uploaders: Julien Danjou , Thomas Goirand , Ghe Rivero , Mehdi Abaakouk -Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~), python-pbr (>= 0.5.16) +Build-Depends: debhelper (>= 9), + openstack-pkg-tools, + python-all (>= 2.6.6-3~), + python-pbr (>= 0.5.21), + python-setuptools Build-Depends-Indep: openstack-pkg-tools, pep8, + python-fixtures (>= 0.3.12), python-httplib2, python-mock, python-nose, - python-pbr (>= 0.5.21), python-prettytable, python-requests (>= 1.1), - python-setuptools, python-setuptools-git, - python-simplejson, + python-simplejson (>= 2.0.9), python-six, python-sphinx, python-subunit, + python-testtools (>= 0.9.32), python-unittest2, testrepository (>= 0.0.17) Standards-Version: 3.9.3 @@ -35,7 +39,7 @@ Depends: python-httplib2, python-pbr (>= 0.5.21), python-prettytable, python-requests (>= 1.1), - python-simplejson, + python-simplejson (>= 2.0.9), python-six, ${misc:Depends}, ${python:Depends} From d4e4e50276aba73e6060bacae6436d84a24d2f43 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sun, 6 Oct 2013 17:28:46 +0000 Subject: [PATCH 106/165] Standards-Version: 3.9.4 --- debian/changelog | 1 + debian/control | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index ec9a172..e7cbc38 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,6 +3,7 @@ python-cinderclient (1:1.0.6-1) unstable; urgency=low * New upstream release. * Fixed new (build-)depends for this release. * Added --slowest --testr-args='--subunit ' when calling testr. + * Bumped Standard-Version: to 3.9.4. -- Thomas Goirand Tue, 01 Oct 2013 15:19:25 +0800 diff --git a/debian/control b/debian/control index 2e99b5d..29b2843 100644 --- a/debian/control +++ b/debian/control @@ -27,7 +27,7 @@ Build-Depends-Indep: openstack-pkg-tools, python-testtools (>= 0.9.32), python-unittest2, testrepository (>= 0.0.17) -Standards-Version: 3.9.3 +Standards-Version: 3.9.4 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary Vcs-Git: git://anonscm.debian.org/openstack/python-cinderclient.git Homepage: https://github.com/openstack/python-cinderclient From c44d5dd294eadc4ddf10eede390d3d1f832844d9 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sun, 6 Oct 2013 17:30:15 +0000 Subject: [PATCH 107/165] Going back to use ./run_tests.sh -P -N || true instead of calling testr directly --- debian/changelog | 1 - debian/rules | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index e7cbc38..5cadae6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,7 +2,6 @@ python-cinderclient (1:1.0.6-1) unstable; urgency=low * New upstream release. * Fixed new (build-)depends for this release. - * Added --slowest --testr-args='--subunit ' when calling testr. * Bumped Standard-Version: to 3.9.4. -- Thomas Goirand Tue, 01 Oct 2013 15:19:25 +0800 diff --git a/debian/rules b/debian/rules index aa1822a..30bd43a 100755 --- a/debian/rules +++ b/debian/rules @@ -27,5 +27,6 @@ override_dh_install: ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) override_dh_auto_test: - python setup.py testr --slowest --testr-args='--subunit ' + ./run_tests.sh -P -N || true + #python setup.py testr --slowest --testr-args='--subunit ' endif From 7848143fc093815bd22a7314a76edddb74a23362 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sun, 6 Oct 2013 17:31:28 +0000 Subject: [PATCH 108/165] Removed duplicate openstack-pkg-tools, build-depends and removed pep8 build-depends. --- debian/control | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 29b2843..c7d59e4 100644 --- a/debian/control +++ b/debian/control @@ -11,9 +11,7 @@ Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~), python-pbr (>= 0.5.21), python-setuptools -Build-Depends-Indep: openstack-pkg-tools, - pep8, - python-fixtures (>= 0.3.12), +Build-Depends-Indep: python-fixtures (>= 0.3.12), python-httplib2, python-mock, python-nose, From 2730b96bc8910888335a792eac2d3c5a60192954 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 7 Oct 2013 07:21:00 +0000 Subject: [PATCH 109/165] Added missing build-depends: python-babel. --- debian/changelog | 6 ++++++ debian/control | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 5cadae6..e247765 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.6-2) unstable; urgency=low + + * Added missing build-depends: python-babel. + + -- Thomas Goirand Mon, 07 Oct 2013 07:20:30 +0000 + python-cinderclient (1:1.0.6-1) unstable; urgency=low * New upstream release. diff --git a/debian/control b/debian/control index c7d59e4..0ae6b50 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,8 @@ Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~), python-pbr (>= 0.5.21), python-setuptools -Build-Depends-Indep: python-fixtures (>= 0.3.12), +Build-Depends-Indep: python-babel, + python-fixtures (>= 0.3.12), python-httplib2, python-mock, python-nose, From f96dcd714e494dc31612e01a9927beded691e2e8 Mon Sep 17 00:00:00 2001 From: jenny-shieh Date: Fri, 20 Sep 2013 12:28:45 -0700 Subject: [PATCH 110/165] Fix the failure of fetching the version in cinder endpoint To search for verion in cinder endpoint string, instead of using hard code position Implements: search for verion in cinder endpoint string Closes-Bug: #1227307 Change-Id: Ie38806ad995e6fd49155f448abf9b2ef43f24a0e --- cinderclient/client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index 4846e4a..ab9b8dd 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -384,13 +384,14 @@ class HTTPClient(object): 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] + components = path.split("/") 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:] + for version in valid_versions: + if version in components: + return version[1:] + msg = "Invalid client version '%s'. must be one of: %s" % ( + (version, ', '.join(valid_versions))) + raise exceptions.UnsupportedVersion(msg) def get_client_class(version): From 98e9df62ee29f6d611214961ca82a162ee312030 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Wed, 9 Oct 2013 17:26:34 -0400 Subject: [PATCH 111/165] Fix DeprecationWarning when printing exception On Python 2.6, this results in an error like: /usr/lib/python2.6/site-packages/cinderclient/shell.py:524: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6 message = e.message Should use the method from novaclient commit 8c4e145b92, which works well with python2 and python3. Change-Id: Ifbd78ad158ae87670bdee4e247469091efaa3260 --- cinderclient/shell.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index c9c1529..89ba346 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -29,8 +29,6 @@ import pkgutil import sys import logging -import six - from cinderclient import client from cinderclient import exceptions as exc import cinderclient.extension @@ -521,10 +519,7 @@ def main(): sys.exit(130) except Exception as e: logger.debug(e, exc_info=1) - message = e.message - if not isinstance(message, six.string_types): - message = str(message) - print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr) + print("ERROR: %s" % strutils.six.text_type(e), file=sys.stderr) sys.exit(1) From 82622357cd066263f39d7625bdf5343b52ba27cb Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Thu, 10 Oct 2013 20:26:37 +0000 Subject: [PATCH 112/165] Updated from global requirements Change-Id: I4f26d3a8790c799e5c1f7f81031406e0c57545f9 Change-Id: Ica7315effe308bd739e90f885849fac27a3c542b --- requirements.txt | 4 ++-- setup.py | 2 +- test-requirements.txt | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index cb57ba5..a40ca9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ argparse PrettyTable>=0.6,<0.8 requests>=1.1 simplejson>=2.0.9 -Babel>=0.9.6 -six +Babel>=1.3 +six>=1.4.1 diff --git a/setup.py b/setup.py index 2a0786a..70c2b3f 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.21,<1.0'], + setup_requires=['pbr'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 30c33c0..b035f15 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,12 +1,12 @@ # Install bounded pep8/pyflakes first, then let flake8 install pep8==1.4.5 -pyflakes==0.7.2 +pyflakes>=0.7.2,<0.7.4 flake8==2.0 -hacking>=0.5.6,<0.7 +hacking>=0.5.6,<0.8 coverage>=3.6 discover -fixtures>=0.3.12 -mock>=0.8.0 +fixtures>=0.3.14 +mock>=1.0 python-subunit sphinx>=1.1.2 testtools>=0.9.32 From b0b8afaf55ffdadf5ee329edd92b9cac1be8c2d1 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 6 Oct 2013 22:39:41 -0400 Subject: [PATCH 113/165] python3: Refactor dict for python2/python3 compat Python3 changed the behavior of dict.keys such that it is now returns a dict_keys object, which is iterable but not indexable. You can get the python2 result back with an explicit call to list. Refactor list(*.keys()) so that it just uses list(). Change-Id: Ib2e9646ac967e9bd7cc4f47e2099f5d1358808a9 Signed-off-by: Chuck Short --- cinderclient/base.py | 2 +- cinderclient/client.py | 2 +- cinderclient/exceptions.py | 2 +- cinderclient/shell.py | 2 +- cinderclient/tests/fakes.py | 5 ++--- cinderclient/tests/v1/fakes.py | 22 +++++++++++----------- cinderclient/tests/v2/fakes.py | 22 +++++++++++----------- cinderclient/v1/quota_classes.py | 2 +- cinderclient/v1/quotas.py | 2 +- cinderclient/v1/shell.py | 10 +++++----- cinderclient/v2/quota_classes.py | 2 +- cinderclient/v2/quotas.py | 2 +- cinderclient/v2/shell.py | 10 +++++----- tools/colorizer.py | 2 +- 14 files changed, 43 insertions(+), 44 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index e1f44eb..73bdec6 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -270,7 +270,7 @@ class Resource(object): return self.__dict__[k] def __repr__(self): - reprkeys = sorted(k for k in list(self.__dict__.keys()) if k[0] != '_' + reprkeys = sorted(k for k in self.__dict__ 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 4846e4a..625066c 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -402,7 +402,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(list(version_map.keys())))) + (version, ', '.join(version_map))) raise exceptions.UnsupportedVersion(msg) return utils.import_class(client_path) diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py index b84eefd..1e3050c 100644 --- a/cinderclient/exceptions.py +++ b/cinderclient/exceptions.py @@ -166,7 +166,7 @@ def from_response(response, body): message = "n/a" details = "n/a" if hasattr(body, 'keys'): - error = body[list(body.keys())[0]] + error = body[list(body)[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/shell.py b/cinderclient/shell.py index c9c1529..54615bb 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -478,7 +478,7 @@ class OpenStackCinderShell(object): options = set() for sc_str, sc in list(self.subcommands.items()): commands.add(sc_str) - for option in list(sc._optionals._option_string_actions.keys()): + for option in sc._optionals._option_string_actions: options.add(option) commands.remove('bash-completion') diff --git a/cinderclient/tests/fakes.py b/cinderclient/tests/fakes.py index 7d80350..5a3937c 100644 --- a/cinderclient/tests/fakes.py +++ b/cinderclient/tests/fakes.py @@ -23,12 +23,11 @@ from __future__ import print_function def assert_has_keys(dict, required=[], optional=[]): - keys = list(dict.keys()) for k in required: try: - assert k in keys + assert k in dict except AssertionError: - extra_keys = set(keys).difference(set(required + optional)) + extra_keys = set(dict).difference(set(required + optional)) raise AssertionError("found unexpected keys: %s" % list(extra_keys)) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 87c9b39..cea11a3 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -268,8 +268,8 @@ class FakeHTTPClient(base_client.HTTPClient): def post_snapshots_1234_action(self, body, **kw): _body = None resp = 202 - assert len(body.keys()) == 1 - action = body.keys()[0] + assert len(list(body)) == 1 + action = list(body)[0] if action == 'os-reset_status': assert 'status' in body['os-reset_status'] elif action == 'os-update_snapshot_status': @@ -313,10 +313,10 @@ class FakeHTTPClient(base_client.HTTPClient): def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 - assert len(list(body.keys())) == 1 - action = list(body.keys())[0] + assert len(list(body)) == 1 + action = list(body)[0] if action == 'os-attach': - assert list(body[action].keys()) == ['instance_uuid', 'mountpoint'] + assert list(body[action]) == ['instance_uuid', 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': @@ -324,10 +324,10 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-unreserve': assert body[action] is None elif action == 'os-initialize_connection': - assert list(body[action].keys()) == ['connector'] + assert list(body[action]) == ['connector'] return (202, {}, {'connection_info': 'foos'}) elif action == 'os-terminate_connection': - assert list(body[action].keys()) == ['connector'] + assert list(body[action]) == ['connector'] elif action == 'os-begin_detaching': assert body[action] is None elif action == 'os-roll_detaching': @@ -335,7 +335,7 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-reset_status': assert 'status' in body[action] elif action == 'os-extend': - assert body[action].keys() == ['new_size'] + assert list(body[action]) == ['new_size'] elif action == 'os-migrate_volume': assert 'host' in body[action] assert 'force_host_copy' in body[action] @@ -370,7 +370,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_sets_test(self, body, **kw): - assert list(body.keys()) == ['quota_set'] + assert list(body) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { @@ -393,7 +393,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_class_sets_test(self, body, **kw): - assert list(body.keys()) == ['quota_class_set'] + assert list(body) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -431,7 +431,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'extra_specs': {}}}) def post_types_1_extra_specs(self, body, **kw): - assert list(body.keys()) == ['extra_specs'] + assert list(body) == ['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 d057456..f848512 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -275,8 +275,8 @@ class FakeHTTPClient(base_client.HTTPClient): def post_snapshots_1234_action(self, body, **kw): _body = None resp = 202 - assert len(body.keys()) == 1 - action = body.keys()[0] + assert len(list(body)) == 1 + action = list(body)[0] if action == 'os-reset_status': assert 'status' in body['os-reset_status'] elif action == 'os-update_snapshot_status': @@ -320,10 +320,10 @@ class FakeHTTPClient(base_client.HTTPClient): def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 - assert len(list(body.keys())) == 1 - action = list(body.keys())[0] + assert len(list(body)) == 1 + action = list(body)[0] if action == 'os-attach': - assert list(body[action].keys()) == ['instance_uuid', 'mountpoint'] + assert list(body[action]) == ['instance_uuid', 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': @@ -331,10 +331,10 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-unreserve': assert body[action] is None elif action == 'os-initialize_connection': - assert list(body[action].keys()) == ['connector'] + assert list(body[action]) == ['connector'] return (202, {}, {'connection_info': 'foos'}) elif action == 'os-terminate_connection': - assert list(body[action].keys()) == ['connector'] + assert list(body[action]) == ['connector'] elif action == 'os-begin_detaching': assert body[action] is None elif action == 'os-roll_detaching': @@ -342,7 +342,7 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-reset_status': assert 'status' in body[action] elif action == 'os-extend': - assert body[action].keys() == ['new_size'] + assert list(body[action]) == ['new_size'] elif action == 'os-migrate_volume': assert 'host' in body[action] assert 'force_host_copy' in body[action] @@ -377,7 +377,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_sets_test(self, body, **kw): - assert list(body.keys()) == ['quota_set'] + assert list(body) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { @@ -400,7 +400,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'gigabytes': 1}}) def put_os_quota_class_sets_test(self, body, **kw): - assert list(body.keys()) == ['quota_class_set'] + assert list(body) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -438,7 +438,7 @@ class FakeHTTPClient(base_client.HTTPClient): 'extra_specs': {}}}) def post_types_1_extra_specs(self, body, **kw): - assert list(body.keys()) == ['extra_specs'] + assert list(body) == ['extra_specs'] return (200, {}, {'extra_specs': {'k': 'v'}}) def delete_types_1_extra_specs_k(self, **kw): diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py index 1f880fb..9e81e2c 100644 --- a/cinderclient/v1/quota_classes.py +++ b/cinderclient/v1/quota_classes.py @@ -39,7 +39,7 @@ class QuotaClassSetManager(base.Manager): def update(self, class_name, **updates): body = {'quota_class_set': {'class_name': class_name}} - for update in updates.keys(): + for update in updates: 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 a0028a9..87d3405 100644 --- a/cinderclient/v1/quotas.py +++ b/cinderclient/v1/quotas.py @@ -40,7 +40,7 @@ class QuotaSetManager(base.Manager): def update(self, tenant_id, **updates): body = {'quota_set': {'tenant_id': tenant_id}} - for update in updates.keys(): + for update in updates: 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 b117824..74f8662 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -95,7 +95,7 @@ def _print_volume_image(image): def _translate_keys(collection, convert): for item in collection: - keys = list(item.__dict__.keys()) + keys = item.__dict__ 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]) @@ -352,7 +352,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, list(metadata.keys())) + cs.volumes.delete_metadata(volume, list(metadata)) @utils.arg( @@ -560,7 +560,7 @@ def do_type_key(cs, args): if args.action == 'set': vtype.set_keys(keypair) elif args.action == 'unset': - vtype.unset_keys(list(keypair.keys())) + vtype.unset_keys(list(keypair)) def do_endpoints(cs, args): @@ -582,7 +582,7 @@ _quota_resources = ['volumes', 'snapshots', 'gigabytes'] def _quota_show(quotas): quota_dict = {} - for resource in quotas._info.keys(): + for resource in quotas._info: good_name = False for name in _quota_resources: if resource.startswith(name): @@ -1198,7 +1198,7 @@ def do_qos_key(cs, args): if args.action == 'set': cs.qos_specs.set_keys(args.qos_specs, keypair) elif args.action == 'unset': - cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + cs.qos_specs.unset_keys(args.qos_specs, list(keypair)) @utils.arg('qos_specs', metavar='', diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index 4e44914..bf80db0 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -37,7 +37,7 @@ class QuotaClassSetManager(base.Manager): def update(self, class_name, **updates): body = {'quota_class_set': {'class_name': class_name}} - for update in updates.keys(): + for update in updates: 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 0972210..7384460 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -38,7 +38,7 @@ class QuotaSetManager(base.Manager): def update(self, tenant_id, **updates): body = {'quota_set': {'tenant_id': tenant_id}} - for update in updates.keys(): + for update in updates: 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 18a6bbb..6ce4e87 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -89,7 +89,7 @@ def _print_volume_image(image): def _translate_keys(collection, convert): for item in collection: - keys = list(item.__dict__.keys()) + keys = item.__dict__ 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]) @@ -390,7 +390,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, list(metadata.keys())) + cs.volumes.delete_metadata(volume, list(metadata)) @utils.arg('--all-tenants', @@ -616,7 +616,7 @@ def do_type_key(cs, args): if args.action == 'set': vtype.set_keys(keypair) elif args.action == 'unset': - vtype.unset_keys(list(keypair.keys())) + vtype.unset_keys(list(keypair)) def do_endpoints(cs, args): @@ -638,7 +638,7 @@ _quota_resources = ['volumes', 'snapshots', 'gigabytes'] def _quota_show(quotas): quota_dict = {} - for resource in quotas._info.keys(): + for resource in quotas._info: good_name = False for name in _quota_resources: if resource.startswith(name): @@ -1273,7 +1273,7 @@ def do_qos_key(cs, args): if args.action == 'set': cs.qos_specs.set_keys(args.qos_specs, keypair) elif args.action == 'unset': - cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + cs.qos_specs.unset_keys(args.qos_specs, list(keypair)) @utils.arg('qos_specs', metavar='', diff --git a/tools/colorizer.py b/tools/colorizer.py index 1b6e576..2a079fc 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 =list(self.results.keys()) + classes =list(self.results) oldest = min(classes, key=lambda x: self.last_time[x]) diff = (self._now() - self.last_time[oldest]).total_seconds() self.writeTestCase(oldest) From 5c462987d22e9f31e6cf2fbd7e6dc19b43ad6f76 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 13 Oct 2013 08:36:03 -0400 Subject: [PATCH 114/165] python3: sort dict for post_volumes_1234_action test In Python 3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. Sort the list and update the expecting result. Change-Id: Iead7ca8f45b035abd78f0b25865b5f7e1b9f3294 Signed-off-by: Chuck Short --- cinderclient/tests/v1/fakes.py | 3 ++- cinderclient/tests/v2/fakes.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index cea11a3..84bbbd4 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -316,7 +316,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert len(list(body)) == 1 action = list(body)[0] if action == 'os-attach': - assert list(body[action]) == ['instance_uuid', 'mountpoint'] + assert sorted(list(body[action])) == ['instance_uuid', + 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index f848512..aa235d9 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -323,7 +323,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert len(list(body)) == 1 action = list(body)[0] if action == 'os-attach': - assert list(body[action]) == ['instance_uuid', 'mountpoint'] + assert sorted(list(body[action])) == ['instance_uuid', + 'mountpoint'] elif action == 'os-detach': assert body[action] is None elif action == 'os-reserve': From fcf8e1d0b91ec28191f96783c367eaedfbd85a61 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 13 Oct 2013 09:47:08 -0400 Subject: [PATCH 115/165] python3: align the order of parameters for urlencode() In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. In the test case, the fixed expecting string will not match the test result, it is relying on the dict order. This change transforms the input dict to a sequence of two-element list, with fixed order, and update the related expecitng string in test case. Change-Id: I60d7bb3c4f940b76460ad5c417a1807915e0418e Signed-off-by: Chuck Short --- cinderclient/v1/volume_snapshots.py | 8 +++++++- cinderclient/v2/volume_snapshots.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index 0aa6495..ea0ed43 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -108,7 +108,13 @@ class SnapshotManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urlencode(qparams) if qparams else "" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % urlencode(new_qparams) + else: + query_string = "" detail = "" if detailed: diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index 4e16ba8..a904f95 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -95,7 +95,13 @@ class SnapshotManager(base.ManagerWithFind): if val: qparams[opt] = val - query_string = "?%s" % urlencode(qparams) if qparams else "" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % urlencode(new_qparams) + else: + query_string = "" detail = "" if detailed: From 68fe7735c014ea0bfbcb27b9d0b7e3ed584d3285 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 13 Oct 2013 09:01:51 -0400 Subject: [PATCH 116/165] python3: iteration order of dict is unpredictable In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. Sort the metadata.keys() in reverse order to keep the test cases as is. Change-Id: I233ada4dae4e9c0bc97bf8fd7d912a0eff9dd5b8 Signed-off-by: Chuck Short --- cinderclient/v1/shell.py | 4 +++- cinderclient/v2/shell.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 74f8662..ff7842b 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -352,7 +352,9 @@ def do_metadata(cs, args): if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': - cs.volumes.delete_metadata(volume, list(metadata)) + # NOTE(zul): Make sure py2/py3 sorting is the same + cs.volumes.delete_metadata(volume, sorted(metadata.keys(), + reverse=True)) @utils.arg( diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6ce4e87..014dbf7 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -390,7 +390,9 @@ def do_metadata(cs, args): if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': - cs.volumes.delete_metadata(volume, list(metadata)) + # NOTE(zul): Make sure py2/py3 sorting is the same + cs.volumes.delete_metadata(volume, sorted(metadata.keys(), + reverse=True)) @utils.arg('--all-tenants', From d3a366fd0d22e9d6038685f54108d4b54d416372 Mon Sep 17 00:00:00 2001 From: saurabh Date: Thu, 19 Sep 2013 21:26:15 +0530 Subject: [PATCH 117/165] Addition of volume/snapshot_metadata CLI Added below CLIs:- 1. Set or Delete metadata of a snapshot 2. Show metadata of a snapshot 3. Show metadata of a volume 4. Update all metadata of volume 5. Update all metadata of snapshot Implements blueprint: add-metadata-cli Change-Id: Ic2b5f3fce6104d1756879718f666a42549458ad3 --- cinderclient/tests/v1/fakes.py | 15 ++++++ cinderclient/tests/v1/test_shell.py | 26 +++++++++ cinderclient/tests/v1/test_volumes.py | 5 ++ cinderclient/tests/v2/fakes.py | 15 ++++++ cinderclient/tests/v2/test_shell.py | 26 +++++++++ cinderclient/tests/v2/test_volumes.py | 5 ++ cinderclient/v1/shell.py | 77 +++++++++++++++++++++++++++ cinderclient/v1/volume_snapshots.py | 42 +++++++++++++++ cinderclient/v1/volumes.py | 16 +++++- cinderclient/v2/shell.py | 77 +++++++++++++++++++++++++++ cinderclient/v2/volume_snapshots.py | 42 +++++++++++++++ cinderclient/v2/volumes.py | 16 +++++- 12 files changed, 360 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index cea11a3..68269db 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -734,3 +734,18 @@ class FakeHTTPClient(base_client.HTTPClient): }, ] }) + + def post_snapshots_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) + + def delete_snapshots_1234_metadata_key1(self, **kw): + return (200, {}, None) + + def delete_snapshots_1234_metadata_key2(self, **kw): + return (200, {}, None) + + def put_volumes_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) + + def put_snapshots_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index a79c413..9f484f1 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -266,3 +266,29 @@ class ShellTest(utils.TestCase): expected = {'os-migrate_volume': {'force_host_copy': 'True', 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_snapshot_metadata_set(self): + self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2') + self.assert_called('POST', '/snapshots/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_snapshot_metadata_unset_dict(self): + self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') + self.assert_called('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + + def test_snapshot_metadata_unset_keys(self): + self.run_command('snapshot-metadata 1234 unset key1 key2') + self.assert_called('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + + def test_volume_metadata_update_all(self): + self.run_command('metadata-update-all 1234 key1=val1 key2=val2') + self.assert_called('PUT', '/volumes/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_snapshot_metadata_update_all(self): + self.run_command('snapshot-metadata-update-all\ + 1234 key1=val1 key2=val2') + self.assert_called('PUT', '/snapshots/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index bb73438..f38fbe6 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -96,3 +96,8 @@ class VolumesTest(utils.TestCase): v = cs.volumes.get('1234') cs.volumes.migrate_volume(v, 'dest', False) cs.assert_called('POST', '/volumes/1234/action') + + def test_metadata_update_all(self): + cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) + cs.assert_called('PUT', '/volumes/1234/metadata', + {'metadata': {'k1': 'v1'}}) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index f848512..f509343 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -742,3 +742,18 @@ class FakeHTTPClient(base_client.HTTPClient): }, ] }) + + def post_snapshots_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) + + def delete_snapshots_1234_metadata_key1(self, **kw): + return (200, {}, None) + + def delete_snapshots_1234_metadata_key2(self, **kw): + return (200, {}, None) + + def put_volumes_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) + + def put_snapshots_1234_metadata(self, **kw): + return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index f18e39d..a6e34b1 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -244,3 +244,29 @@ class ShellTest(utils.TestCase): expected = {'os-migrate_volume': {'force_host_copy': 'True', 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_snapshot_metadata_set(self): + self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2') + self.assert_called('POST', '/snapshots/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_snapshot_metadata_unset_dict(self): + self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') + self.assert_called('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + + def test_snapshot_metadata_unset_keys(self): + self.run_command('snapshot-metadata 1234 unset key1 key2') + self.assert_called('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + + def test_volume_metadata_update_all(self): + self.run_command('metadata-update-all 1234 key1=val1 key2=val2') + self.assert_called('PUT', '/volumes/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_snapshot_metadata_update_all(self): + self.run_command('snapshot-metadata-update-all\ + 1234 key1=val1 key2=val2') + self.assert_called('PUT', '/snapshots/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 7b85369..7e44f84 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -99,3 +99,8 @@ class VolumesTest(utils.TestCase): v = cs.volumes.get('1234') cs.volumes.migrate_volume(v, 'dest', False) cs.assert_called('POST', '/volumes/1234/action') + + def test_metadata_update_all(self): + cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) + cs.assert_called('PUT', '/volumes/1234/metadata', + {'metadata': {'k1': 'v1'}}) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 74f8662..3da380d 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1208,3 +1208,80 @@ def do_qos_get_association(cs, args): """Get all associations of specific qos specs.""" associations = cs.qos_specs.get_associations(args.qos_specs) _print_associations_list(associations) + + +@utils.arg('snapshot', + metavar='', + help='ID of the snapshot to update metadata on.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata to set/unset (only key is necessary on unset)') +@utils.service_type('volume') +def do_snapshot_metadata(cs, args): + """Set or Delete metadata of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + + if args.action == 'set': + metadata = snapshot.set_metadata(metadata) + utils.print_dict(metadata._info) + elif args.action == 'unset': + snapshot.delete_metadata(list(metadata.keys())) + + +@utils.arg('snapshot', metavar='', + help='ID of snapshot') +@utils.service_type('volume') +def do_snapshot_metadata_show(cs, args): + """Show metadata of given snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + utils.print_dict(snapshot._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', metavar='', + help='ID of volume') +@utils.service_type('volume') +def do_metadata_show(cs, args): + """Show metadata of given volume.""" + volume = utils.find_volume(cs, args.volume) + utils.print_dict(volume._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', + metavar='', + help='ID of the volume to update metadata on.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata entry/entries to update.') +@utils.service_type('volume') +def do_metadata_update_all(cs, args): + """Update all metadata of a volume.""" + volume = utils.find_volume(cs, args.volume) + metadata = _extract_metadata(args) + metadata = volume.update_all_metadata(metadata) + utils.print_dict(metadata) + + +@utils.arg('snapshot', + metavar='', + help='ID of the snapshot to update metadata on.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata entry/entries to update.') +@utils.service_type('volume') +def do_snapshot_metadata_update_all(cs, args): + """Update all metadata of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + metadata = snapshot.update_all_metadata(metadata) + utils.print_dict(metadata) diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py index 0aa6495..997fb35 100644 --- a/cinderclient/v1/volume_snapshots.py +++ b/cinderclient/v1/volume_snapshots.py @@ -57,6 +57,18 @@ class Snapshot(base.Resource): """Update the snapshot with the privided state.""" self.manager.reset_state(self, state) + def set_metadata(self, metadata): + """Set metadata of this snapshot.""" + return self.manager.set_metadata(self, metadata) + + def delete_metadata(self, keys): + """Delete metadata of this snapshot.""" + return self.manager.delete_metadata(self, keys) + + def update_all_metadata(self, metadata): + """Update_all metadata of this snapshot.""" + return self.manager.update_all_metadata(self, metadata) + class SnapshotManager(base.ManagerWithFind): """ @@ -152,3 +164,33 @@ class SnapshotManager(base.ManagerWithFind): def update_snapshot_status(self, snapshot, update_dict): return self._action('os-update_snapshot_status', base.getid(snapshot), update_dict) + + def set_metadata(self, snapshot, metadata): + """Update/Set a snapshots metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be set. + """ + body = {'metadata': metadata} + return self._create("/snapshots/%s/metadata" % base.getid(snapshot), + body, "metadata") + + def delete_metadata(self, snapshot, keys): + """Delete specified keys from snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param keys: A list of keys to be removed. + """ + snapshot_id = base.getid(snapshot) + for k in keys: + self._delete("/snapshots/%s/metadata/%s" % (snapshot_id, k)) + + def update_all_metadata(self, snapshot, metadata): + """Update_all snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/snapshots/%s/metadata" % base.getid(snapshot), + body) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 6804d59..36758c7 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -123,6 +123,10 @@ class Volume(base.Resource): # self.manager.migrate_volume_completion(self, old_volume, # new_volume, error) + def update_all_metadata(self, metadata): + """Update all metadata of this volume.""" + return self.manager.update_all_metadata(self, metadata) + class VolumeManager(base.ManagerWithFind): """ @@ -331,7 +335,7 @@ class VolumeManager(base.ManagerWithFind): Delete specified keys from volumes metadata. :param volume: The :class:`Volume`. - :param metadata: A list of keys to be removed. + :param keys: A list of keys to be removed. """ for k in keys: self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k)) @@ -395,3 +399,13 @@ class VolumeManager(base.ManagerWithFind): return self._action('os-migrate_volume_completion', old_volume, {'new_volume': new_volume_id, 'error': error})[1] + + def update_all_metadata(self, volume, metadata): + """Update all metadata of a volume. + + :param volume: The :class:`Volume`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/volumes/%s/metadata" % base.getid(volume), + body) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6ce4e87..0000f0e 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1283,3 +1283,80 @@ def do_qos_get_association(cs, args): """Get all associations of specific qos specs.""" associations = cs.qos_specs.get_associations(args.qos_specs) _print_associations_list(associations) + + +@utils.arg('snapshot', + metavar='', + help='ID of the snapshot to update metadata on.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata to set/unset (only key is necessary on unset)') +@utils.service_type('volumev2') +def do_snapshot_metadata(cs, args): + """Set or Delete metadata of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + + if args.action == 'set': + metadata = snapshot.set_metadata(metadata) + utils.print_dict(metadata._info) + elif args.action == 'unset': + snapshot.delete_metadata(list(metadata.keys())) + + +@utils.arg('snapshot', metavar='', + help='ID of snapshot') +@utils.service_type('volumev2') +def do_snapshot_metadata_show(cs, args): + """Show metadata of given snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + utils.print_dict(snapshot._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', metavar='', + help='ID of volume') +@utils.service_type('volumev2') +def do_metadata_show(cs, args): + """Show metadata of given volume.""" + volume = utils.find_volume(cs, args.volume) + utils.print_dict(volume._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', + metavar='', + help='ID of the volume to update metadata on.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata entry/entries to update.') +@utils.service_type('volumev2') +def do_metadata_update_all(cs, args): + """Update all metadata of a volume.""" + volume = utils.find_volume(cs, args.volume) + metadata = _extract_metadata(args) + metadata = volume.update_all_metadata(metadata) + utils.print_dict(metadata) + + +@utils.arg('snapshot', + metavar='', + help='ID of the snapshot to update metadata on.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata entry/entries to update') +@utils.service_type('volumev2') +def do_snapshot_metadata_update_all(cs, args): + """Update all metadata of a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + metadata = snapshot.update_all_metadata(metadata) + utils.print_dict(metadata) diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index 4e16ba8..f04a334 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -49,6 +49,18 @@ class Snapshot(base.Resource): """Update the snapshot with the provided state.""" self.manager.reset_state(self, state) + def set_metadata(self, metadata): + """Set metadata of this snapshot.""" + return self.manager.set_metadata(self, metadata) + + def delete_metadata(self, keys): + """Delete metadata of this snapshot.""" + return self.manager.delete_metadata(self, keys) + + def update_all_metadata(self, metadata): + """Update_all metadata of this snapshot.""" + return self.manager.update_all_metadata(self, metadata) + class SnapshotManager(base.ManagerWithFind): """Manage :class:`Snapshot` resources.""" @@ -137,3 +149,33 @@ class SnapshotManager(base.ManagerWithFind): def update_snapshot_status(self, snapshot, update_dict): return self._action('os-update_snapshot_status', base.getid(snapshot), update_dict) + + def set_metadata(self, snapshot, metadata): + """Update/Set a snapshots metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be set. + """ + body = {'metadata': metadata} + return self._create("/snapshots/%s/metadata" % base.getid(snapshot), + body, "metadata") + + def delete_metadata(self, snapshot, keys): + """Delete specified keys from snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param keys: A list of keys to be removed. + """ + snapshot_id = base.getid(snapshot) + for k in keys: + self._delete("/snapshots/%s/metadata/%s" % (snapshot_id, k)) + + def update_all_metadata(self, snapshot, metadata): + """Update_all snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/snapshots/%s/metadata" % base.getid(snapshot), + body) diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index e948cff..e6dae97 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -121,6 +121,10 @@ class Volume(base.Resource): # self.manager.migrate_volume_completion(self, old_volume, # new_volume, error) + def update_all_metadata(self, metadata): + """Update all metadata of this volume.""" + return self.manager.update_all_metadata(self, metadata) + class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" @@ -314,7 +318,7 @@ class VolumeManager(base.ManagerWithFind): """Delete specified keys from volumes metadata. :param volume: The :class:`Volume`. - :param metadata: A list of keys to be removed. + :param keys: A list of keys to be removed. """ for k in keys: self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k)) @@ -377,3 +381,13 @@ class VolumeManager(base.ManagerWithFind): return self._action('os-migrate_volume_completion', old_volume, {'new_volume': new_volume_id, 'error': error})[1] + + def update_all_metadata(self, volume, metadata): + """Update all metadata of a volume. + + :param volume: The :class:`Volume`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/volumes/%s/metadata" % base.getid(volume), + body) From c4ae3dd7a5edcab3050e13c89746ef121ff8518b Mon Sep 17 00:00:00 2001 From: Edward Hope-Morley Date: Tue, 15 Oct 2013 18:09:42 +0100 Subject: [PATCH 118/165] Fixes broken v1 and v2 api backup-restore Also adds unit tests. Change-Id: I741889d49cffe46bf9165b266a62f2b328354e01 Fixes: bug 1240151 --- cinderclient/tests/v1/fakes.py | 4 ++++ cinderclient/tests/v1/test_shell.py | 8 ++++++++ cinderclient/tests/v2/fakes.py | 4 ++++ cinderclient/tests/v2/test_shell.py | 8 ++++++++ cinderclient/v1/shell.py | 4 ++-- cinderclient/v2/shell.py | 4 ++-- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index cea11a3..5e2ab26 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -533,6 +533,10 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + def post_backups_1234_restore(self, **kw): + return (200, {}, + {'restore': _stub_restore()}) + # # QoSSpecs # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index a79c413..26571da 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -117,6 +117,14 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_backup(self): + self.run_command('backup-create 1234') + self.assert_called('POST', '/backups') + + def test_restore(self): + self.run_command('backup-restore 1234') + self.assert_called('POST', '/backups/1234/restore') + def test_snapshot_list_filter_volume_id(self): self.run_command('snapshot-list --volume-id=1234') self.assert_called('GET', '/snapshots/detail?volume_id=1234') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index f848512..e90da80 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -540,6 +540,10 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + def post_backups_1234_restore(self, **kw): + return (200, {}, + {'restore': _stub_restore()}) + # # QoSSpecs # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index f18e39d..566f620 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -95,6 +95,14 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_backup(self): + self.run_command('backup-create 1234') + self.assert_called('POST', '/backups') + + def test_restore(self): + self.run_command('backup-restore 1234') + self.assert_called('POST', '/backups/1234/restore') + def test_snapshot_list_filter_volume_id(self): self.run_command('snapshot-list --volume-id=1234') self.assert_called('GET', '/snapshots/detail?volume_id=1234') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 74f8662..e0658d5 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -812,8 +812,8 @@ def do_backup_delete(cs, args): @utils.service_type('volume') def do_backup_restore(cs, args): """Restore a backup.""" - if args.volume: - volume_id = utils.find_volume(cs, args.volume).id + if args.volume_id: + volume_id = utils.find_volume(cs, args.volume_id).id else: volume_id = None cs.restores.restore(args.backup, volume_id) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 6ce4e87..3ee4bb6 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -903,8 +903,8 @@ def do_backup_delete(cs, args): @utils.service_type('volumev2') def do_backup_restore(cs, args): """Restore a backup.""" - if args.volume: - volume_id = utils.find_volume(cs, args.volume).id + if args.volume_id: + volume_id = utils.find_volume(cs, args.volume_id).id else: volume_id = None cs.restores.restore(args.backup, volume_id) From 84ea630be01cc8a847ba73daa90f609fcc5aa00b Mon Sep 17 00:00:00 2001 From: "Jay S. Bryant" Date: Tue, 1 Oct 2013 13:34:00 -0500 Subject: [PATCH 119/165] Override endpoint URL check for API version get_volume_api_version_from_endpoint currently assumes that the management_url will always be in the format of http://:/vX/. Customers using a gateway, however, can get a mangement_url in a different form like https://://vX/. This causes the get_volume_api_version_from_endpoint to find the resource portion of the url when looking for the Volume API version which makes the cinder client unusable. To resolve this I have implemented an exception handler in the case that get_volume_api_version_from_endpoint fails. If the exception is thrown, I check for OS_VOLUME_API_VERSION in the environment and use the value of that environment variable, if it is set, to determine the API version to use. There are proposals to fix this via changes to keystone in future releases, but this provides a way for IaaS Gateway users to proceed with Havana. Change-Id: I8b978a6ecd87b81c6ae34938e37623ad1cc33994 Closes-Bug: 1233311 --- cinderclient/shell.py | 50 +++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 8dadd9a..8592cb8 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -1,5 +1,5 @@ -# Copyright (c) 2011 OpenStack Foundation +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -41,6 +41,7 @@ DEFAULT_OS_VOLUME_API_VERSION = "1" DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL' DEFAULT_CINDER_SERVICE_TYPE = 'volume' +logging.basicConfig() logger = logging.getLogger(__name__) @@ -173,7 +174,7 @@ class OpenStackCinderShell(object): parser.add_argument('--os-volume-api-version', metavar='', default=utils.env('OS_VOLUME_API_VERSION', - default=DEFAULT_OS_VOLUME_API_VERSION), + default=None), help='Accepts 1 or 2,defaults ' 'to env[OS_VOLUME_API_VERSION].') parser.add_argument('--os_volume_api_version', @@ -327,7 +328,7 @@ class OpenStackCinderShell(object): streamhandler = logging.StreamHandler() streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" streamhandler.setFormatter(logging.Formatter(streamformat)) - logger.setLevel(logging.DEBUG) + logger.setLevel(logging.WARNING) logger.addHandler(streamhandler) def main(self, argv): @@ -335,6 +336,14 @@ class OpenStackCinderShell(object): parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) self.setup_debugging(options.debug) + api_version_input = True + + if not options.os_volume_api_version: + # Environment variable OS_VOLUME_API_VERSION was + # not set and '--os-volume-api-version' option doesn't + # specify a value. Fall back to default. + options.os_volume_api_version = DEFAULT_OS_VOLUME_API_VERSION + api_version_input = False # build available subcommands based on version self.extensions = self._discover_extensions( @@ -450,14 +459,33 @@ 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) + endpoint_api_version = None + # Try to get the API version from the endpoint URL. If that fails fall + # back to trying to use what the user specified via + # --os-volume-api-version or with the OS_VOLUME_API_VERSION environment + # variable. Fail safe is to use the default API setting. + try: + 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) + except exc.UnsupportedVersion: + endpoint_api_version = options.os_volume_api_version + if api_version_input: + logger.warning("Unable to determine the API version via " + "endpoint URL. Falling back to user " + "specified version: %s" % + endpoint_api_version) + else: + logger.warning("Unable to determine the API version from " + "endpoint URL or user input. Falling back to " + "default API version: %s" % + endpoint_api_version) args.func(self.cs, args) From 6b998a95299e19c1b4a4f2f2c7a7f907bf950cb4 Mon Sep 17 00:00:00 2001 From: Edward Hope-Morley Date: Tue, 22 Oct 2013 16:36:17 -0700 Subject: [PATCH 120/165] Deprecates --volume-id arg for v2 backup-restore This patch deprecates the --volume-id arg of backup-restore when using the v2 api and adds (preferred) --volume arg. Both args are still usable but the former is now listed as "Deprecated" in the help message. Change-Id: I2575d93041b26f971c59bc04828b356556617b62 Fixes: bug 1242816 --- cinderclient/v2/shell.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 848a061..d233604 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -926,13 +926,17 @@ def do_backup_delete(cs, args): @utils.arg('backup', metavar='', help='ID of the backup to restore.') @utils.arg('--volume-id', metavar='', + help=argparse.SUPPRESS, + default=None) +@utils.arg('--volume', metavar='', help='Optional ID(or name) of the volume to restore to.', default=None) @utils.service_type('volumev2') def do_backup_restore(cs, args): """Restore a backup.""" - if args.volume_id: - volume_id = utils.find_volume(cs, args.volume_id).id + vol = args.volume or args.volume_id + if vol: + volume_id = utils.find_volume(cs, vol).id else: volume_id = None cs.restores.restore(args.backup, volume_id) From f6f3254d8bbc216a2dc549dc3cff0ecf56a44ebf Mon Sep 17 00:00:00 2001 From: Zhi Yan Liu Date: Mon, 2 Sep 2013 17:53:06 +0800 Subject: [PATCH 121/165] Adding Read-Only volume attaching support to Cinder client Adding mode argument to volume manager attach function. Allow Nova using read-write or read-only mode to attach a volume to an instance. blueprint read-only-volumes Change-Id: Ib20c7802304d83f4aafef522171ebd47f672d883 Signed-off-by: Zhi Yan Liu --- cinderclient/tests/v1/fakes.py | 1 + cinderclient/tests/v1/test_volumes.py | 2 +- cinderclient/tests/v2/fakes.py | 1 + cinderclient/tests/v2/test_volumes.py | 2 +- cinderclient/v1/volumes.py | 11 +++++++---- cinderclient/v2/volumes.py | 11 +++++++---- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 6520da6..16b725a 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -321,6 +321,7 @@ class FakeHTTPClient(base_client.HTTPClient): action = list(body)[0] if action == 'os-attach': assert sorted(list(body[action])) == ['instance_uuid', + 'mode', 'mountpoint'] elif action == 'os-detach': assert body[action] is None diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index f38fbe6..33c62e3 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -35,7 +35,7 @@ class VolumesTest(utils.TestCase): def test_attach(self): v = cs.volumes.get('1234') - cs.volumes.attach(v, 1, '/dev/vdc') + cs.volumes.attach(v, 1, '/dev/vdc', mode='rw') cs.assert_called('POST', '/volumes/1234/action') def test_detach(self): diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 53bb358..891787f 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -328,6 +328,7 @@ class FakeHTTPClient(base_client.HTTPClient): action = list(body)[0] if action == 'os-attach': assert sorted(list(body[action])) == ['instance_uuid', + 'mode', 'mountpoint'] elif action == 'os-detach': assert body[action] is None diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 7e44f84..4e3a8af 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -38,7 +38,7 @@ class VolumesTest(utils.TestCase): def test_attach(self): v = cs.volumes.get('1234') - cs.volumes.attach(v, 1, '/dev/vdc') + cs.volumes.attach(v, 1, '/dev/vdc', mode='ro') cs.assert_called('POST', '/volumes/1234/action') def test_detach(self): diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 36758c7..dbff995 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -38,13 +38,14 @@ class Volume(base.Resource): """Update the display_name or display_description for this volume.""" self.manager.update(self, **kwargs) - def attach(self, instance_uuid, mountpoint): + def attach(self, instance_uuid, mountpoint, mode='rw'): """Set attachment metadata. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance. + :param mode: the access mode """ - return self.manager.attach(self, instance_uuid, mountpoint) + return self.manager.attach(self, instance_uuid, mountpoint, mode) def detach(self): """Clear attachment metadata.""" @@ -240,7 +241,7 @@ class VolumeManager(base.ManagerWithFind): url = '/volumes/%s/action' % base.getid(volume) return self.api.client.post(url, body=body) - def attach(self, volume, instance_uuid, mountpoint): + def attach(self, volume, instance_uuid, mountpoint, mode='rw'): """ Set attachment metadata. @@ -248,11 +249,13 @@ class VolumeManager(base.ManagerWithFind): you would like to attach. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance. + :param mode: the access mode. """ return self._action('os-attach', volume, {'instance_uuid': instance_uuid, - 'mountpoint': mountpoint}) + 'mountpoint': mountpoint, + 'mode': mode}) def detach(self, volume): """ diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index e6dae97..bd5c268 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -37,13 +37,14 @@ class Volume(base.Resource): """Update the name or description for this volume.""" self.manager.update(self, **kwargs) - def attach(self, instance_uuid, mountpoint): + def attach(self, instance_uuid, mountpoint, mode='rw'): """Set attachment metadata. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance. + :param mode: the access mode. """ - return self.manager.attach(self, instance_uuid, mountpoint) + return self.manager.attach(self, instance_uuid, mountpoint, mode) def detach(self): """Clear attachment metadata.""" @@ -233,18 +234,20 @@ class VolumeManager(base.ManagerWithFind): url = '/volumes/%s/action' % base.getid(volume) return self.api.client.post(url, body=body) - def attach(self, volume, instance_uuid, mountpoint): + def attach(self, volume, instance_uuid, mountpoint, mode='rw'): """Set attachment metadata. :param volume: The :class:`Volume` (or its ID) you would like to attach. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance. + :param mode: the access mode. """ return self._action('os-attach', volume, {'instance_uuid': instance_uuid, - 'mountpoint': mountpoint}) + 'mountpoint': mountpoint, + 'mode': mode}) def detach(self, volume): """Clear attachment metadata. From 4ce8e800c408583fe58b429fb249b2c202a65f0a Mon Sep 17 00:00:00 2001 From: Jay Lau Date: Fri, 25 Oct 2013 22:57:03 -0400 Subject: [PATCH 122/165] Enable "cinder delete" can delete multiple volumes in one request "nova delete" can delete multiple servers in one request but "cinder delete" can only delete one volume in one request, it is better to enhance cinder client to support remove multiple volumes in one request. Change-Id: I6a63aa3d7e4c152ae5e45bf2b36d862bdcd6df33 Closes-Bug: #1241941 --- cinderclient/tests/v1/fakes.py | 11 +++++++-- cinderclient/tests/v1/test_shell.py | 4 ++++ cinderclient/tests/v2/fakes.py | 11 +++++++-- cinderclient/tests/v2/test_shell.py | 4 ++++ cinderclient/v1/shell.py | 36 +++++++++++++++++++++-------- cinderclient/v2/shell.py | 36 +++++++++++++++++++++-------- 6 files changed, 78 insertions(+), 24 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 260bcaa..b1d4821 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -297,13 +297,17 @@ class FakeHTTPClient(base_client.HTTPClient): # at the very least it's not complete def get_volumes_detail(self, **kw): return (200, {}, {"volumes": [ - {'id': 1234, + {'id': kw.get('id', 1234), 'name': 'sample-volume', 'attachments': [{'server_id': 1234}]}, ]}) def get_volumes_1234(self, **kw): - r = {'volume': self.get_volumes_detail()[2]['volumes'][0]} + r = {'volume': self.get_volumes_detail(id=1234)[2]['volumes'][0]} + return (200, {}, r) + + def get_volumes_5678(self, **kw): + r = {'volume': self.get_volumes_detail(id=5678)[2]['volumes'][0]} return (200, {}, r) def get_volumes_1234_encryption(self, **kw): @@ -350,6 +354,9 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_volumes_1234(self, **kw): return (202, {}, None) + def delete_volumes_5678(self, **kw): + return (202, {}, None) + # # Quotas # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 26571da..740342d 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -117,6 +117,10 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_delete_multiple(self): + self.run_command('delete 1234 5678') + self.assert_called('DELETE', '/volumes/5678') + def test_backup(self): self.run_command('backup-create 1234') self.assert_called('POST', '/backups') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 35351b2..174369d 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -304,13 +304,17 @@ class FakeHTTPClient(base_client.HTTPClient): # at the very least it's not complete def get_volumes_detail(self, **kw): return (200, {}, {"volumes": [ - {'id': 1234, + {'id': kw.get('id', 1234), 'name': 'sample-volume', 'attachments': [{'server_id': 1234}]}, ]}) def get_volumes_1234(self, **kw): - r = {'volume': self.get_volumes_detail()[2]['volumes'][0]} + r = {'volume': self.get_volumes_detail(id=1234)[2]['volumes'][0]} + return (200, {}, r) + + def get_volumes_5678(self, **kw): + r = {'volume': self.get_volumes_detail(id=5678)[2]['volumes'][0]} return (200, {}, r) def get_volumes_1234_encryption(self, **kw): @@ -357,6 +361,9 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_volumes_1234(self, **kw): return (202, {}, None) + def delete_volumes_5678(self, **kw): + return (202, {}, None) + # # Quotas # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 566f620..26a389c 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -95,6 +95,10 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_delete_multiple(self): + self.run_command('delete 1234 5678') + self.assert_called('DELETE', '/volumes/5678') + def test_backup(self): self.run_command('backup-create 1234') self.assert_called('POST', '/backups') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 2e90d5b..820dc87 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -277,22 +277,38 @@ def do_create(cs, args): _print_volume(volume) -@utils.arg('volume', metavar='', - help='Name or ID of the volume to delete.') +@utils.arg('volume', metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volume') def do_delete(cs, args): - """Remove a volume.""" - volume = utils.find_volume(cs, args.volume) - volume.delete() + """Remove volume(s).""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).delete() + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to delete any of the specified " + "volumes.") -@utils.arg('volume', metavar='', - help='Name or ID of the volume to delete.') +@utils.arg('volume', metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volume') def do_force_delete(cs, args): - """Attempt forced removal of a volume, regardless of its state.""" - volume = utils.find_volume(cs, args.volume) - volume.force_delete() + """Attempt forced removal of volume(s), regardless of the state(s).""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).force_delete() + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to force delete any of the " + "specified volumes.") @utils.arg('volume', metavar='', diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index d233604..cd23ab4 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -304,23 +304,39 @@ def do_create(cs, args): @utils.arg('volume', - metavar='', - help='Name or ID of the volume to delete.') + metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volumev2') def do_delete(cs, args): - """Remove a volume.""" - volume = utils.find_volume(cs, args.volume) - volume.delete() + """Remove a volume(s).""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).delete() + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to delete any of the specified " + "volumes.") @utils.arg('volume', - metavar='', - help='Name or ID of the volume to delete.') + metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volumev2') def do_force_delete(cs, args): - """Attempt forced removal of a volume, regardless of its state.""" - volume = utils.find_volume(cs, args.volume) - volume.force_delete() + """Attempt forced removal of volume(s), regardless of the state(s).""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).force_delete() + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to force delete any of the " + "specified volumes.") @utils.arg('volume', metavar='', From 7ea6c8eb806cbe439a31144921c20433c8023af2 Mon Sep 17 00:00:00 2001 From: Zhi Yan Liu Date: Thu, 5 Sep 2013 13:46:50 +0800 Subject: [PATCH 123/165] Adding volume readonly-mode-update interface to Cinder client Adding volume readonly-mode-update interface to shell and client object (v1 and v2) to allow end user update volume read-only access mode flag. blueprint read-only-volumes Related-Id: I4c84614d6541d5f7c358abadb957da7b8c3d9c48 Change-Id: I2d587f301a21c0d42b8940465862da25a70ed5a8 Signed-off-by: Zhi Yan Liu --- cinderclient/tests/v1/fakes.py | 2 ++ cinderclient/tests/v1/test_shell.py | 9 +++++++++ cinderclient/tests/v1/test_volumes.py | 5 +++++ cinderclient/tests/v2/fakes.py | 2 ++ cinderclient/tests/v2/test_shell.py | 9 +++++++++ cinderclient/tests/v2/test_volumes.py | 5 +++++ cinderclient/v1/shell.py | 14 ++++++++++++++ cinderclient/v1/volumes.py | 17 +++++++++++++++-- cinderclient/v2/shell.py | 14 ++++++++++++++ cinderclient/v2/volumes.py | 14 ++++++++++++++ 10 files changed, 89 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 6520da6..a5c34ec 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -344,6 +344,8 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-migrate_volume': assert 'host' in body[action] assert 'force_host_copy' in body[action] + elif action == 'os-update_readonly_flag': + assert body[action].keys() == ['readonly'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index edc5d7b..3b431b9 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -304,3 +304,12 @@ class ShellTest(utils.TestCase): 1234 key1=val1 key2=val2') self.assert_called('PUT', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_readonly_mode_update(self): + self.run_command('readonly-mode-update 1234 True') + expected = {'os-update_readonly_flag': {'readonly': True}} + self.assert_called('POST', '/volumes/1234/action', body=expected) + + self.run_command('readonly-mode-update 1234 False') + expected = {'os-update_readonly_flag': {'readonly': False}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/tests/v1/test_volumes.py b/cinderclient/tests/v1/test_volumes.py index f38fbe6..a72e6d8 100644 --- a/cinderclient/tests/v1/test_volumes.py +++ b/cinderclient/tests/v1/test_volumes.py @@ -101,3 +101,8 @@ class VolumesTest(utils.TestCase): cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) cs.assert_called('PUT', '/volumes/1234/metadata', {'metadata': {'k1': 'v1'}}) + + def test_readonly_mode_update(self): + v = cs.volumes.get('1234') + cs.volumes.update_readonly_flag(v, True) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 53bb358..ef59289 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -351,6 +351,8 @@ class FakeHTTPClient(base_client.HTTPClient): elif action == 'os-migrate_volume': assert 'host' in body[action] assert 'force_host_copy' in body[action] + elif action == 'os-update_readonly_flag': + assert body[action].keys() == ['readonly'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 58d8de3..cc29113 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -282,3 +282,12 @@ class ShellTest(utils.TestCase): 1234 key1=val1 key2=val2') self.assert_called('PUT', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_readonly_mode_update(self): + self.run_command('readonly-mode-update 1234 True') + expected = {'os-update_readonly_flag': {'readonly': True}} + self.assert_called('POST', '/volumes/1234/action', body=expected) + + self.run_command('readonly-mode-update 1234 False') + expected = {'os-update_readonly_flag': {'readonly': False}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 7e44f84..24dd019 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -104,3 +104,8 @@ class VolumesTest(utils.TestCase): cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) cs.assert_called('PUT', '/volumes/1234/metadata', {'metadata': {'k1': 'v1'}}) + + def test_readonly_mode_update(self): + v = cs.volumes.get('1234') + cs.volumes.update_readonly_flag(v, True) + cs.assert_called('POST', '/volumes/1234/action') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 3a40dbf..0012388 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1329,3 +1329,17 @@ def do_snapshot_metadata_update_all(cs, args): metadata = _extract_metadata(args) metadata = snapshot.update_all_metadata(metadata) utils.print_dict(metadata) + + +@utils.arg('volume', metavar='', help='ID of the volume to update.') +@utils.arg('read_only', + metavar='', + choices=['True', 'true', 'False', 'false'], + help='Flag to indicate whether to update volume to ' + 'read-only access mode.') +@utils.service_type('volume') +def do_readonly_mode_update(cs, args): + """Update volume read-only access mode read_only.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.update_readonly_flag(volume, + strutils.bool_from_string(args.read_only)) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 36758c7..7a70d34 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -108,10 +108,9 @@ class Volume(base.Resource): def extend(self, volume, new_size): """Extend the size of the specified volume. - :param volume: The UUID of the volume to extend + :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) def migrate_volume(self, host, force_host_copy): @@ -127,6 +126,15 @@ class Volume(base.Resource): """Update all metadata of this volume.""" return self.manager.update_all_metadata(self, metadata) + def update_readonly_flag(self, volume, read_only): + """Update the read-only access mode flag of the specified volume. + + :param volume: The UUID of the volume to update. + :param read_only: The value to indicate whether to update volume to + read-only access mode. + """ + self.manager.update_readonly_flag(self, volume, read_only) + class VolumeManager(base.ManagerWithFind): """ @@ -409,3 +417,8 @@ class VolumeManager(base.ManagerWithFind): body = {'metadata': metadata} return self._update("/volumes/%s/metadata" % base.getid(volume), body) + + def update_readonly_flag(self, volume, flag): + return self._action('os-update_readonly_flag', + base.getid(volume), + {'readonly': flag}) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index fdec4cc..bce7585 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1408,3 +1408,17 @@ def do_snapshot_metadata_update_all(cs, args): metadata = _extract_metadata(args) metadata = snapshot.update_all_metadata(metadata) utils.print_dict(metadata) + + +@utils.arg('volume', metavar='', help='ID of the volume to update.') +@utils.arg('read_only', + metavar='', + choices=['True', 'true', 'False', 'false'], + help='Flag to indicate whether to update volume to ' + 'read-only access mode.') +@utils.service_type('volumev2') +def do_readonly_mode_update(cs, args): + """Update volume read-only access mode flag.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.update_readonly_flag(volume, + strutils.bool_from_string(args.read_only)) diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index e6dae97..91ae436 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -125,6 +125,15 @@ class Volume(base.Resource): """Update all metadata of this volume.""" return self.manager.update_all_metadata(self, metadata) + def update_readonly_flag(self, volume, read_only): + """Update the read-only access mode flag of the specified volume. + + :param volume: The UUID of the volume to update. + :param read_only: The value to indicate whether to update volume to + read-only access mode. + """ + self.manager.update_readonly_flag(self, volume, read_only) + class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" @@ -391,3 +400,8 @@ class VolumeManager(base.ManagerWithFind): body = {'metadata': metadata} return self._update("/volumes/%s/metadata" % base.getid(volume), body) + + def update_readonly_flag(self, volume, flag): + return self._action('os-update_readonly_flag', + base.getid(volume), + {'readonly': flag}) From 8a6a291e033e0d191ac8702f05fa3e83206d61f0 Mon Sep 17 00:00:00 2001 From: "Jay S. Bryant" Date: Fri, 25 Oct 2013 11:43:46 -0500 Subject: [PATCH 124/165] Enable del of other tenants resources by name Currently, due to the way that resources are being retrieved by the findall() function, an administrator can do a list, snapshot-list, etc. with the --all_tenants option and see other tenants' resources. If the admin then tries to delete the another tenants' resource by name, it fails with a 'No with a name or ID of exists.' error. The solution to this is to change the call to the list() function in findall() to set the all_tenants search option to 1. This causes the admin to get a list of all the resources that they have access to back when the search is done instead of just a list of their resources. The delete by name is then possible. The server takes care of ensuring that only resources that the user has access to are returned. This will enable delete by name for all resources that use the find_resource function. Closes-bug: 1241682 Change-Id: I4e9957b66c11b7e1081f066d189cedc5a3cb2a6c --- cinderclient/base.py | 5 ++++- cinderclient/tests/test_utils.py | 2 +- cinderclient/tests/v1/test_shell.py | 5 +++++ cinderclient/tests/v2/test_shell.py | 5 +++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 73bdec6..30dbbd9 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -203,7 +203,10 @@ class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)): found = [] searches = list(kwargs.items()) - for obj in self.list(): + # Want to search for all tenants here so that when attempting to delete + # that a user like admin doesn't get a failure when trying to delete + # another tenant's volume by name. + for obj in self.list(search_opts={'all_tenants': 1}): try: if all(getattr(obj, attr) == value for (attr, value) in searches): diff --git a/cinderclient/tests/test_utils.py b/cinderclient/tests/test_utils.py index 32db04c..92aee51 100644 --- a/cinderclient/tests/test_utils.py +++ b/cinderclient/tests/test_utils.py @@ -55,7 +55,7 @@ class FakeManager(base.ManagerWithFind): return resource raise exceptions.NotFound(resource_id) - def list(self): + def list(self, search_opts): return self.resources diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 3b431b9..6294036 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -117,6 +117,11 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_delete_by_name(self): + self.run_command('delete sample-volume') + self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1') + self.assert_called('DELETE', '/volumes/1234') + def test_delete_multiple(self): self.run_command('delete 1234 5678') self.assert_called('DELETE', '/volumes/5678') diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index cc29113..5928296 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -95,6 +95,11 @@ class ShellTest(utils.TestCase): self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + def test_delete_by_name(self): + self.run_command('delete sample-volume') + self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1') + self.assert_called('DELETE', '/volumes/1234') + def test_delete_multiple(self): self.run_command('delete 1234 5678') self.assert_called('DELETE', '/volumes/5678') From bdd560b3c02ea0a013ed4b7e7641c6240f3b602f Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Wed, 30 Oct 2013 10:20:17 +0200 Subject: [PATCH 125/165] Fix py33 due to readonly and metadata patches A couple recent merges broke py33 - this fixes it. Change-Id: Iaab8a08aeb42c954f3ecff7da70003d1c4eead0d --- cinderclient/tests/v1/fakes.py | 2 +- cinderclient/tests/v1/test_shell.py | 8 ++++---- cinderclient/tests/v2/fakes.py | 2 +- cinderclient/tests/v2/test_shell.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 0540c81..111683a 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -346,7 +346,7 @@ class FakeHTTPClient(base_client.HTTPClient): assert 'host' in body[action] assert 'force_host_copy' in body[action] elif action == 'os-update_readonly_flag': - assert body[action].keys() == ['readonly'] + assert list(body[action]) == ['readonly'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 3b431b9..432059f 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -286,13 +286,13 @@ class ShellTest(utils.TestCase): def test_snapshot_metadata_unset_dict(self): self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') - self.assert_called('DELETE', '/snapshots/1234/metadata/key1') - self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') def test_snapshot_metadata_unset_keys(self): self.run_command('snapshot-metadata 1234 unset key1 key2') - self.assert_called('DELETE', '/snapshots/1234/metadata/key1') - self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') def test_volume_metadata_update_all(self): self.run_command('metadata-update-all 1234 key1=val1 key2=val2') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 066a14c..5804d0f 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -353,7 +353,7 @@ class FakeHTTPClient(base_client.HTTPClient): assert 'host' in body[action] assert 'force_host_copy' in body[action] elif action == 'os-update_readonly_flag': - assert body[action].keys() == ['readonly'] + assert list(body[action]) == ['readonly'] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index cc29113..4a5c33d 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -264,13 +264,13 @@ class ShellTest(utils.TestCase): def test_snapshot_metadata_unset_dict(self): self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') - self.assert_called('DELETE', '/snapshots/1234/metadata/key1') - self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') def test_snapshot_metadata_unset_keys(self): self.run_command('snapshot-metadata 1234 unset key1 key2') - self.assert_called('DELETE', '/snapshots/1234/metadata/key1') - self.assert_called('DELETE', '/snapshots/1234/metadata/key2', pos=-2) + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') + self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') def test_volume_metadata_update_all(self): self.run_command('metadata-update-all 1234 key1=val1 key2=val2') From d21ed05b4ed1a5b5321876c481e0286d3696afd5 Mon Sep 17 00:00:00 2001 From: john-griffith Date: Wed, 30 Oct 2013 11:36:37 -0600 Subject: [PATCH 126/165] Update version and index.rst for push Update prior to push 1.0.7 to PyPi. Includes changes/updates to doc/source/index and adds link to README.rst Change-Id: I666f3e6004885db7a688abdac88fa1154d83f8c9 --- README.rst | 2 ++ doc/source/index.rst | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/README.rst b/README.rst index 86a8669..1e86f26 100644 --- a/README.rst +++ b/README.rst @@ -148,3 +148,5 @@ Quick-start using keystone:: >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="volume") >>> nt.volumes.list() [...] + +See release notes and more at ``_. diff --git a/doc/source/index.rst b/doc/source/index.rst index e72aed1..0171fc2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,6 +29,23 @@ See also :doc:`/man/cinder`. Release Notes ============= +1.0.7 +----- +* Add support for read-only volumes +* Add support for setting snapshot metadata +* Deprecate volume-id arg to backup restore in favor of --volume +* Add quota-usage command +* Fix exception deprecation warning message +* Report error when no args supplied to rename cmd + +.. _1241941: http://bugs.launchpad.net/python-cinderclient/+bug/1241941 +.. _1242816: http://bugs.launchpad.net/python-cinderclient/+bug/1242816 +.. _1233311: http://bugs.launchpad.net/python-cinderclient/+bug/1233311 +.. _1227307: http://bugs.launchpad.net/python-cinderclient/+bug/1227307 +.. _1240151: http://bugs.launchpad.net/python-cinderclient/+bug/1240151 +.. _1241682: http://bugs.launchpad.net/python-cinderclient/+bug/1241682 + + 1.0.6 ----- * Add support for multiple endpoints @@ -38,6 +55,7 @@ Release Notes * Add update action for snapshot metadata * Add encryption metadata support * Add volume migrate support +* Add support for QoS specs .. _1221104: http://bugs.launchpad.net/python-cinderclient/+bug/1221104 .. _1220590: http://bugs.launchpad.net/python-cinderclient/+bug/1220590 From 6296974b1ad78a5ff56fb5068a38084bcef1f424 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 11 Nov 2013 10:59:20 -0800 Subject: [PATCH 127/165] Update link in HACKING.rst and Make it DRYer * Fix URL for global hacking doc, related to I579e7c889f3addc2cd40bce0c584bbc70bf435e2 * Remove section on locals, as its already in openstack-dev/hacking (http://git.openstack.org/cgit/openstack-dev/hacking/tree/doc/source/index.rst#n154) Change-Id: If944b088f343404c5b90b02afe6f781dd1db914d --- HACKING.rst | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index ed887f5..59eeb19 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -2,7 +2,7 @@ Cinder Client Style Commandments ========================= - Step 1: Read the OpenStack Style Commandments - https://github.com/openstack-dev/hacking/blob/master/HACKING.rst + http://docs.openstack.org/developer/hacking/ - Step 2: Read on Cinder Client Specific Commandments @@ -10,15 +10,6 @@ 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: From cd191d2eb36960ef878bf85a0a3e426f321ff261 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Thu, 14 Nov 2013 19:30:46 +0200 Subject: [PATCH 128/165] Add index.rst section for patches merged to master Add new section called "MASTER" to the release notes, where all patches that implement blueprints or fix bugs should update the list. This list will be used for the release notes when a new version is released. Change-Id: If842bfbb85851fb7a9e22259423da204f4c308f4 --- doc/source/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 0171fc2..2c0543b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,6 +29,9 @@ See also :doc:`/man/cinder`. Release Notes ============= +MASTER +------ + 1.0.7 ----- * Add support for read-only volumes From 6844e54f13b741d2a4b0b118a4a5e7d1cc122ed0 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Thu, 24 Oct 2013 07:53:29 +0200 Subject: [PATCH 129/165] change assertEquals to assertEqual According to http://docs.python.org/2/library/unittest.html assertEquals is a deprecated alias of assertEqual. https://review.openstack.org/#/c/56652/ updates to a new version of hacking that enforces H602. Turning that on requires this change to land first. Re-do of https://review.openstack.org/#/c/53510 since that is abandoned. Change-Id: Ie621edc8770e2015df5712f77d4cf9679b02bd0f --- cinderclient/tests/test_service_catalog.py | 32 +++++++++++----------- cinderclient/tests/v1/test_shell.py | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cinderclient/tests/test_service_catalog.py b/cinderclient/tests/test_service_catalog.py index 1055962..d815557 100644 --- a/cinderclient/tests/test_service_catalog.py +++ b/cinderclient/tests/test_service_catalog.py @@ -240,10 +240,10 @@ class ServiceCatalogTest(utils.TestCase): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='compute') - self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'), - "https://compute1.host/v1/1234") - self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'), - "https://compute1.host/v1/3456") + self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'), + "https://compute1.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'), + "https://compute1.host/v1/3456") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", "South", service_type='compute') @@ -253,15 +253,15 @@ class ServiceCatalogTest(utils.TestCase): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='volume') - self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'), - "https://volume1.host/v1/1234") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), - "https://volume1.host/v1/3456") + self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'), + "https://volume1.host/v1/1234") + self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'), + "https://volume1.host/v1/3456") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volumev2'), - "https://volume1.host/v2/3456") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volumev2'), - "https://volume1.host/v2/3456") + self.assertEqual(sc.url_for('tenantId', '2', service_type='volumev2'), + "https://volume1.host/v2/3456") + self.assertEqual(sc.url_for('tenantId', '2', service_type='volumev2'), + "https://volume1.host/v2/3456") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", "North", service_type='volume') @@ -269,7 +269,7 @@ class ServiceCatalogTest(utils.TestCase): def test_compatibility_service_type(self): sc = service_catalog.ServiceCatalog(SERVICE_COMPATIBILITY_CATALOG) - self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'), - "https://volume1.host/v2/1234") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), - "https://volume1.host/v2/3456") + self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'), + "https://volume1.host/v2/1234") + self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'), + "https://volume1.host/v2/3456") diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index c995410..1df5585 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -86,7 +86,7 @@ class ShellTest(utils.TestCase): for input in inputs: args = Arguments(metadata=input[0]) - self.assertEquals(shell_v1._extract_metadata(args), input[1]) + self.assertEqual(shell_v1._extract_metadata(args), input[1]) def test_list(self): self.run_command('list') From 6e214b2c03a98b0284b75988ca07a047eb1d6806 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Fri, 15 Nov 2013 16:50:41 +0000 Subject: [PATCH 130/165] Updated from global requirements Change-Id: I80d6e02b5daaa0862e4b1f15d1bf3211dc0f359e --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b035f15..a00cd7d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ pep8==1.4.5 pyflakes>=0.7.2,<0.7.4 flake8==2.0 -hacking>=0.5.6,<0.8 +hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 From 38388f892ba2dda4af6bf697f549c2a0d5b3e29c Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Tue, 19 Nov 2013 08:11:58 -0500 Subject: [PATCH 131/165] Add encryption-type-delete to cinderclient This modification adds support for deleting volume encryption types. The encryption-type-delete command accepts a single argument, the name or ID of the volume type whose associated encryption type should be deleted. The encryption type may only be deleted if the associated volume type is unused. blueprint encrypt-cinder-volumes Change-Id: Ie189e7ec91f5a8adc9d3496911de2811ad40f282 --- cinderclient/tests/v1/fakes.py | 3 +++ cinderclient/tests/v1/test_shell.py | 8 +++++++- cinderclient/tests/v1/test_volume_encryption_types.py | 7 ++++++- cinderclient/tests/v2/fakes.py | 3 +++ cinderclient/tests/v2/test_shell.py | 8 +++++++- cinderclient/tests/v2/test_volume_encryption_types.py | 7 ++++++- cinderclient/v1/shell.py | 11 +++++++++++ cinderclient/v1/volume_encryption_types.py | 3 ++- cinderclient/v2/shell.py | 11 +++++++++++ cinderclient/v2/volume_encryption_types.py | 3 ++- 10 files changed, 58 insertions(+), 6 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 111683a..9c556ea 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -468,6 +468,9 @@ class FakeHTTPClient(base_client.HTTPClient): def put_types_1_encryption_1(self, body, **kw): return (200, {}, {}) + def delete_types_1_encryption_provider(self, **kw): + return (202, {}, None) + # # Set/Unset metadata # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 1df5585..7476536 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -275,8 +275,14 @@ class ShellTest(utils.TestCase): def test_encryption_type_delete(self): """ Test encryption-type-delete shell command. + + Verify one GET/one DELETE requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one DELETE request to delete the encryption type information """ - self.skipTest("Not implemented") + self.run_command('encryption-type-delete 1') + self.assert_called('DELETE', '/types/1/encryption/provider') + self.assert_called_anytime('GET', '/types/1') def test_migrate_volume(self): self.run_command('migrate 1234 fakehost --force-host-copy=True') diff --git a/cinderclient/tests/v1/test_volume_encryption_types.py b/cinderclient/tests/v1/test_volume_encryption_types.py index d9af7d8..0d9868c 100644 --- a/cinderclient/tests/v1/test_volume_encryption_types.py +++ b/cinderclient/tests/v1/test_volume_encryption_types.py @@ -91,5 +91,10 @@ class VolumeEncryptionTypesTest(utils.TestCase): def test_delete(self): """ Unit test for VolumeEncryptionTypesManager.delete + + Verify that one DELETE request is made for encryption type deletion + Verify that encryption type deletion returns None """ - self.skipTest("Not implemented") + result = cs.volume_encryption_types.delete(1) + cs.assert_called('DELETE', '/types/1/encryption/provider') + self.assertIsNone(result, "delete result must be None") diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 5804d0f..bc88805 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -475,6 +475,9 @@ class FakeHTTPClient(base_client.HTTPClient): def put_types_1_encryption_1(self, body, **kw): return (200, {}, {}) + def delete_types_1_encryption_provider(self, **kw): + return (202, {}, None) + # # Set/Unset metadata # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 44fb2b1..9fc19d1 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -253,8 +253,14 @@ class ShellTest(utils.TestCase): def test_encryption_type_delete(self): """ Test encryption-type-delete shell command. + + Verify one GET/one DELETE requests are made per command invocation: + - one GET request to retrieve the relevant volume type information + - one DELETE request to delete the encryption type information """ - self.skipTest("Not implemented") + self.run_command('encryption-type-delete 1') + self.assert_called('DELETE', '/types/1/encryption/provider') + self.assert_called_anytime('GET', '/types/1') def test_migrate_volume(self): self.run_command('migrate 1234 fakehost --force-host-copy=True') diff --git a/cinderclient/tests/v2/test_volume_encryption_types.py b/cinderclient/tests/v2/test_volume_encryption_types.py index 96a0c02..0d609ea 100644 --- a/cinderclient/tests/v2/test_volume_encryption_types.py +++ b/cinderclient/tests/v2/test_volume_encryption_types.py @@ -91,5 +91,10 @@ class VolumeEncryptionTypesTest(utils.TestCase): def test_delete(self): """ Unit test for VolumeEncryptionTypesManager.delete + + Verify that one DELETE request is made for encryption type deletion + Verify that encryption type deletion returns None """ - self.skipTest("Not implemented") + result = cs.volume_encryption_types.delete(1) + cs.assert_called('DELETE', '/types/1/encryption/provider') + self.assertIsNone(result, "delete result must be None") diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 0012388..f6c52a8 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -1116,6 +1116,17 @@ def do_encryption_type_create(cs, args): _print_volume_encryption_type_list([result]) +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.service_type('volume') +def do_encryption_type_delete(cs, args): + """Delete the encryption type for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + cs.volume_encryption_types.delete(volume_type) + + @utils.arg('volume', metavar='', help='ID of the volume to migrate') @utils.arg('host', metavar='', help='Destination host') @utils.arg('--force-host-copy', metavar='', diff --git a/cinderclient/v1/volume_encryption_types.py b/cinderclient/v1/volume_encryption_types.py index b97c6f0..62892f5 100644 --- a/cinderclient/v1/volume_encryption_types.py +++ b/cinderclient/v1/volume_encryption_types.py @@ -93,4 +93,5 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind): :param volume_type: the volume type whose encryption type information must be deleted """ - raise NotImplementedError() + return self._delete("/types/%s/encryption/provider" % + base.getid(volume_type)) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index bce7585..2919d10 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1211,6 +1211,17 @@ def do_encryption_type_create(cs, args): _print_volume_encryption_type_list([result]) +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.service_type('volumev2') +def do_encryption_type_delete(cs, args): + """Delete the encryption type for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + cs.volume_encryption_types.delete(volume_type) + + def _print_qos_specs(qos_specs): utils.print_dict(qos_specs._info) diff --git a/cinderclient/v2/volume_encryption_types.py b/cinderclient/v2/volume_encryption_types.py index b97c6f0..62892f5 100644 --- a/cinderclient/v2/volume_encryption_types.py +++ b/cinderclient/v2/volume_encryption_types.py @@ -93,4 +93,5 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind): :param volume_type: the volume type whose encryption type information must be deleted """ - raise NotImplementedError() + return self._delete("/types/%s/encryption/provider" % + base.getid(volume_type)) From 6050cfae7ecc8ba5495c1f852e2ae15971f106c0 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Wed, 20 Nov 2013 17:57:21 +0200 Subject: [PATCH 132/165] Update HACKING.rst with release note requirement New patches to python-cinderclient must now add an entry for release notes. Change-Id: Id7fe24f2cabf4145933e0babc6641d250165ce74 --- HACKING.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 59eeb19..a48ac28 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,12 +1,12 @@ Cinder Client Style Commandments -========================= +================================ - Step 1: Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ - Step 2: Read on Cinder Client Specific Commandments ----------------------------- +----------------------------------- General ------- @@ -21,7 +21,7 @@ General raise # OKAY Text encoding ----------- +------------- - All text within python code should be of type 'unicode'. WRONG: @@ -59,3 +59,19 @@ Text encoding returntext = do_some_magic_with(mytext) returnstring = returntext.encode('utf-8') outfile.write(returnstring) + +Release Notes +------------- +- Each patch should add an entry in the doc/source/index.rst file under + "MASTER". + +- On each new release, the entries under "MASTER" will become the release notes + for that release, and "MASTER" will be cleared. + +- The format should match existing release notes. For example, a feature:: + + * Add support for function foo + + Or a bug fix:: + + .. _1241941: http://bugs.launchpad.net/python-cinderclient/+bug/1241941 From e8a0e6abdf29a85516474aadffdce4400e74dce9 Mon Sep 17 00:00:00 2001 From: wanghao Date: Tue, 26 Nov 2013 11:42:17 +0800 Subject: [PATCH 133/165] Fix inappropriate comment for set_metadata Fix inappropriate comment for the param of set_metadata Change-Id: I01495f59b11ed88389b0e0f2f5fb34289f64dbac Closes-Bug: 1254951 --- cinderclient/v1/volumes.py | 2 +- cinderclient/v2/volumes.py | 2 +- doc/source/index.rst | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index 0881a22..a74c835 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -84,7 +84,7 @@ class Volume(base.Resource): def set_metadata(self, volume, metadata): """Set or Append metadata to a volume. - :param type : The :class: `Volume` to set metadata on + :param volume : The :class: `Volume` to set metadata on :param metadata: A dict of key/value pairs to set """ return self.manager.set_metadata(self, metadata) diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 983ec6e..4f4a2bb 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -83,7 +83,7 @@ class Volume(base.Resource): def set_metadata(self, volume, metadata): """Set or Append metadata to a volume. - :param type : The :class: `Volume` to set metadata on + :param volume : The :class: `Volume` to set metadata on :param metadata: A dict of key/value pairs to set """ return self.manager.set_metadata(self, metadata) diff --git a/doc/source/index.rst b/doc/source/index.rst index 2c0543b..74b08e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -31,6 +31,7 @@ Release Notes ============= MASTER ------ +.. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 1.0.7 ----- From 3447532ba65386976a877df775d964d86e5bca88 Mon Sep 17 00:00:00 2001 From: Stephen Mulcahy Date: Wed, 27 Nov 2013 11:04:43 +0000 Subject: [PATCH 134/165] Fix string representation of VolumeBackupsRestore Change string representation of VolumeBackupsRestore to use volume_id rather than non-existent VolumeBackupsRestore id. Change-Id: I7b585d0de9b3b0511e7fd1a70b214090a17dfa93 Closes-Bug: #1255488 --- cinderclient/v1/volume_backups_restore.py | 2 +- cinderclient/v2/volume_backups_restore.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cinderclient/v1/volume_backups_restore.py b/cinderclient/v1/volume_backups_restore.py index faf9e09..0eafa82 100644 --- a/cinderclient/v1/volume_backups_restore.py +++ b/cinderclient/v1/volume_backups_restore.py @@ -24,7 +24,7 @@ from cinderclient import base class VolumeBackupsRestore(base.Resource): """A Volume Backups Restore represents a restore operation.""" def __repr__(self): - return "" % self.id + return "" % self.volume_id class VolumeBackupRestoreManager(base.Manager): diff --git a/cinderclient/v2/volume_backups_restore.py b/cinderclient/v2/volume_backups_restore.py index faf9e09..0eafa82 100644 --- a/cinderclient/v2/volume_backups_restore.py +++ b/cinderclient/v2/volume_backups_restore.py @@ -24,7 +24,7 @@ from cinderclient import base class VolumeBackupsRestore(base.Resource): """A Volume Backups Restore represents a restore operation.""" def __repr__(self): - return "" % self.id + return "" % self.volume_id class VolumeBackupRestoreManager(base.Manager): From 5aaa68f09e4457b78ef08e9bdb1476fc982392b0 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Mon, 25 Nov 2013 18:08:01 +0800 Subject: [PATCH 135/165] Fix typo in cinderclient sematics --> semantics hypen-separated --> hyphen-separated typicaly --> typically Change-Id: I5df277ef7036082d0e4b079c23d41da809e5270f Closes-Bug: #1254587 --- cinderclient/base.py | 4 ++-- cinderclient/shell.py | 2 +- cinderclient/utils.py | 2 +- doc/source/index.rst | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cinderclient/base.py b/cinderclient/base.py index 30dbbd9..280286c 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -110,7 +110,7 @@ class Manager(utils.HookableMixin): try: os.makedirs(cache_dir, 0o755) except OSError: - # NOTE(kiall): This is typicaly either permission denied while + # NOTE(kiall): This is typically either permission denied while # attempting to create the directory, or the directory # already exists. Either way, don't fail. pass @@ -124,7 +124,7 @@ class Manager(utils.HookableMixin): try: setattr(self, cache_attr, open(path, mode)) except IOError: - # NOTE(kiall): This is typicaly a permission denied while + # NOTE(kiall): This is typically a permission denied while # attempting to write the cache file. pass diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 8592cb8..385b05d 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -298,7 +298,7 @@ class OpenStackCinderShell(object): def _find_actions(self, subparsers, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. + # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' diff --git a/cinderclient/utils.py b/cinderclient/utils.py index 7a728d7..a941873 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -56,7 +56,7 @@ def add_arg(f, *args, **kwargs): # NOTE(sirp): avoid dups that can occur when the module is shared across # tests. if (args, kwargs) not in f.arguments: - # Because of the sematics of decorator composition if we just append + # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. f.arguments.insert(0, (args, kwargs)) diff --git a/doc/source/index.rst b/doc/source/index.rst index 74b08e7..52317a4 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -32,6 +32,7 @@ Release Notes MASTER ------ .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 +.. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 1.0.7 ----- From c33dd1b842199874b90d6c0536a87115d09963f6 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Tue, 29 Oct 2013 18:01:41 +0200 Subject: [PATCH 136/165] Add assert to delete multiple test Make sure calls are made to delete both volumes. Also removed redundant fields in v1 fake volume. Closes-Bug: #1253142 Change-Id: I51078041c6d765f28edb155d44928286ee2b0dd6 --- cinderclient/tests/v1/fakes.py | 2 -- cinderclient/tests/v1/test_shell.py | 1 + cinderclient/tests/v2/test_shell.py | 1 + doc/source/index.rst | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 111683a..c3c6bf5 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -35,8 +35,6 @@ def _stub_volume(**kwargs): "bootable": "false", "availability_zone": "cinder", "created_at": "2012-08-27T00:00:00.000000", - "display_description": None, - "display_name": None, "id": '00000000-0000-0000-0000-000000000000', "metadata": {}, "size": 1, diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 1df5585..0f80062 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -124,6 +124,7 @@ class ShellTest(utils.TestCase): def test_delete_multiple(self): self.run_command('delete 1234 5678') + self.assert_called_anytime('DELETE', '/volumes/1234') self.assert_called('DELETE', '/volumes/5678') def test_backup(self): diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 44fb2b1..5ee4e16 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -102,6 +102,7 @@ class ShellTest(utils.TestCase): def test_delete_multiple(self): self.run_command('delete 1234 5678') + self.assert_called_anytime('DELETE', '/volumes/1234') self.assert_called('DELETE', '/volumes/5678') def test_backup(self): diff --git a/doc/source/index.rst b/doc/source/index.rst index 52317a4..0a299bb 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -33,6 +33,7 @@ MASTER ------ .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 +.. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 1.0.7 ----- From e64973f9a70fba10b25645c74454fc3d744568bf Mon Sep 17 00:00:00 2001 From: Vincent Hou Date: Wed, 20 Nov 2013 04:34:38 -0500 Subject: [PATCH 137/165] Add search_opts into the method list() for VolumeTypeManager Since the method base.ManagerWithFind.findall has been modified by adding the search_opts for all tenants support, the parameter search_opts should also be added to all the manager classes, which can be called whithin this method. volumetypemanager does not have this parameter for its method list(). Adding search_opts will resolve this issue. Closes-Bug: #1252665 Change-Id: I526500625f1b5483cb5d88ea15e6ac8485a66ae3 --- cinderclient/v1/volume_types.py | 2 +- cinderclient/v2/volume_types.py | 2 +- doc/source/index.rst | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cinderclient/v1/volume_types.py b/cinderclient/v1/volume_types.py index 12c4612..e8b2c39 100644 --- a/cinderclient/v1/volume_types.py +++ b/cinderclient/v1/volume_types.py @@ -80,7 +80,7 @@ class VolumeTypeManager(base.ManagerWithFind): """ resource_class = VolumeType - def list(self): + def list(self, search_opts=None): """ Get a list of all volume types. diff --git a/cinderclient/v2/volume_types.py b/cinderclient/v2/volume_types.py index bc382bd..3d76302 100644 --- a/cinderclient/v2/volume_types.py +++ b/cinderclient/v2/volume_types.py @@ -70,7 +70,7 @@ class VolumeTypeManager(base.ManagerWithFind): """Manage :class:`VolumeType` resources.""" resource_class = VolumeType - def list(self): + def list(self, search_opts=None): """Get a list of all volume types. :rtype: list of :class:`VolumeType`. diff --git a/doc/source/index.rst b/doc/source/index.rst index 0a299bb..44cc757 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -34,6 +34,7 @@ MASTER .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 +.. _1252665: http://bugs.launchpad.net/python-cinderclient/+bug/1252665 1.0.7 ----- From 8fc5ae480eafffc7e1bea046ad9b8e06350b008e Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Thu, 28 Nov 2013 22:13:47 +0800 Subject: [PATCH 138/165] Ignore swap files generated during file edting by vim To ignore swap files from getting into repository currently the implemented ignore is *.swp however vim adds more swap files if these files exists, so improving this with .*.sw? Change-Id: I11965d5fb22aeae622dad8bb29cf1f0533ed439a Closes-Bug: #1255876 --- .gitignore | 2 +- doc/source/index.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2c64f15..ba0f4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ subunit.log cover *.pyc .idea -*.swp +.*.sw? *~ AUTHORS ChangeLog diff --git a/doc/source/index.rst b/doc/source/index.rst index 44cc757..79d3b22 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -35,6 +35,7 @@ MASTER .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 .. _1252665: http://bugs.launchpad.net/python-cinderclient/+bug/1252665 +.. _1255876: http://bugs.launchpad.net/python-cinderclient/+bug/1255876 1.0.7 ----- From 0e2bd33265de840787a3d9e31f7a14e83c44acf9 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Mon, 2 Dec 2013 18:05:54 -0500 Subject: [PATCH 139/165] Reset-state and snapshot-reset-state for multiple objects This allows a user to call reset-state or snapshot-reset-state on a list of objects. The behavior is modeled after a similar change to delete multiple volumes. $ cinder reset-state good_volume good_volume_2 Error behavior is as follows: One success/one failure: $ cinder reset-state good_volume asdf Reset state for volume asdf failed: No volume with a name or ID of 'asdf' exists. One failure: $ cinder reset-state asdf ERROR: Reset state for volume asdf failed: No volume with a name or ID of 'asdf' exists. Two failures: $ cinder reset-state asdf qwert Reset state for volume asdf failed: No volume with a name or ID of 'asdf' exists. Reset state for volume qwert failed: No volume with a name or ID of 'qwert' exists. ERROR: Unable to reset the state for any of the specified volumes. Related-Bug: 1241941 Closes-Bug: 1256069 Change-Id: Id0a36fb7de0d69be0dac98ea04e4708775250b7a --- cinderclient/tests/v1/fakes.py | 9 ++++++ cinderclient/tests/v1/test_shell.py | 16 +++++++++++ cinderclient/tests/v2/fakes.py | 9 ++++++ cinderclient/tests/v2/test_shell.py | 16 +++++++++++ cinderclient/v1/shell.py | 43 +++++++++++++++++++++++++---- cinderclient/v2/shell.py | 43 +++++++++++++++++++++++++---- doc/source/index.rst | 2 ++ 7 files changed, 126 insertions(+), 12 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index c3c6bf5..1bebecb 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -258,6 +258,9 @@ class FakeHTTPClient(base_client.HTTPClient): def get_snapshots_1234(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='1234')}) + def get_snapshots_5678(self, **kw): + return (200, {}, {'snapshot': _stub_snapshot(id='5678')}) + def put_snapshots_1234(self, **kw): snapshot = _stub_snapshot(id='1234') snapshot.update(kw['body']['snapshot']) @@ -276,6 +279,9 @@ class FakeHTTPClient(base_client.HTTPClient): raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) + def post_snapshots_5678_action(self, body, **kw): + return self.post_snapshots_1234_action(body, **kw) + # # Volumes # @@ -349,6 +355,9 @@ class FakeHTTPClient(base_client.HTTPClient): raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) + def post_volumes_5678_action(self, body, **kw): + return self.post_volumes_1234_action(body, **kw) + def post_volumes(self, **kw): return (202, {}, {'volume': {}}) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 0f80062..a679e4c 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -212,6 +212,14 @@ class ShellTest(utils.TestCase): expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + def test_reset_state_multiple(self): + self.run_command('reset-state 1234 5678 --state error') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called_anytime('POST', '/volumes/1234/action', + body=expected) + self.assert_called_anytime('POST', '/volumes/5678/action', + body=expected) + def test_snapshot_reset_state(self): self.run_command('snapshot-reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} @@ -222,6 +230,14 @@ class ShellTest(utils.TestCase): expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + def test_snapshot_reset_state_multiple(self): + self.run_command('snapshot-reset-state 1234 5678') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called_anytime('POST', '/snapshots/1234/action', + body=expected) + self.assert_called_anytime('POST', '/snapshots/5678/action', + body=expected) + def test_encryption_type_list(self): """ Test encryption-type-list shell command. diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 5804d0f..01b9404 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -267,6 +267,9 @@ class FakeHTTPClient(base_client.HTTPClient): def get_snapshots_1234(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='1234')}) + def get_snapshots_5678(self, **kw): + return (200, {}, {'snapshot': _stub_snapshot(id='5678')}) + def put_snapshots_1234(self, **kw): snapshot = _stub_snapshot(id='1234') snapshot.update(kw['body']['snapshot']) @@ -285,6 +288,9 @@ class FakeHTTPClient(base_client.HTTPClient): raise AssertionError('Unexpected action: %s' % action) return (resp, {}, _body) + def post_snapshots_5678_action(self, body, **kw): + return self.post_snapshots_1234_action(body, **kw) + # # Volumes # @@ -358,6 +364,9 @@ class FakeHTTPClient(base_client.HTTPClient): raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) + def post_volumes_5678_action(self, body, **kw): + return self.post_volumes_1234_action(body, **kw) + def post_volumes(self, **kw): return (202, {}, {'volume': {}}) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 5ee4e16..eccfef9 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -190,6 +190,14 @@ class ShellTest(utils.TestCase): expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + def test_reset_state_multiple(self): + self.run_command('reset-state 1234 5678 --state error') + expected = {'os-reset_status': {'status': 'error'}} + self.assert_called_anytime('POST', '/volumes/1234/action', + body=expected) + self.assert_called_anytime('POST', '/volumes/5678/action', + body=expected) + def test_snapshot_reset_state(self): self.run_command('snapshot-reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} @@ -200,6 +208,14 @@ class ShellTest(utils.TestCase): expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + def test_snapshot_reset_state_multiple(self): + self.run_command('snapshot-reset-state 1234 5678') + expected = {'os-reset_status': {'status': 'available'}} + self.assert_called_anytime('POST', '/snapshots/1234/action', + body=expected) + self.assert_called_anytime('POST', '/snapshots/5678/action', + body=expected) + def test_encryption_type_list(self): """ Test encryption-type-list shell command. diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 0012388..acefe26 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -311,7 +311,7 @@ def do_force_delete(cs, args): "specified volumes.") -@utils.arg('volume', metavar='', +@utils.arg('volume', metavar='', nargs='+', help='Name or ID of the volume to modify.') @utils.arg('--state', metavar='', default='available', help=('Indicate which state to assign the volume. Options include ' @@ -320,8 +320,23 @@ def do_force_delete(cs, args): @utils.service_type('volume') def do_reset_state(cs, args): """Explicitly update the state of a volume.""" - volume = utils.find_volume(cs, args.volume) - volume.reset_state(args.state) + failure_count = 0 + + single = (len(args.volume) == 1) + + for volume in args.volume: + try: + utils.find_volume(cs, volume).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for volume %s failed: %s" % (volume, e) + if not single: + print(msg) + + if failure_count == len(args.volume): + if not single: + msg = "Unable to reset the state for any of the specified volumes." + raise exceptions.CommandError(msg) @utils.arg('volume', metavar='', @@ -498,7 +513,7 @@ def do_snapshot_rename(cs, args): _find_volume_snapshot(cs, args.snapshot).update(**kwargs) -@utils.arg('snapshot', metavar='', +@utils.arg('snapshot', metavar='', nargs='+', help='Name or ID of the snapshot to modify.') @utils.arg('--state', metavar='', default='available', @@ -509,8 +524,24 @@ def do_snapshot_rename(cs, args): @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) + failure_count = 0 + + single = (len(args.snapshot) == 1) + + for snapshot in args.snapshot: + try: + _find_volume_snapshot(cs, snapshot).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for snapshot %s failed: %s" % (snapshot, e) + if not single: + print(msg) + + if failure_count == len(args.snapshot): + if not single: + msg = ("Unable to reset the state for any of the the specified " + "snapshots.") + raise exceptions.CommandError(msg) def _print_volume_type_list(vtypes): diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index bce7585..ddf92c2 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -339,7 +339,7 @@ def do_force_delete(cs, args): "specified volumes.") -@utils.arg('volume', metavar='', +@utils.arg('volume', metavar='', nargs='+', help='Name or ID of the volume to modify.') @utils.arg('--state', metavar='', default='available', help=('Indicate which state to assign the volume. Options include ' @@ -348,8 +348,23 @@ def do_force_delete(cs, args): @utils.service_type('volumev2') def do_reset_state(cs, args): """Explicitly update the state of a volume.""" - volume = utils.find_volume(cs, args.volume) - volume.reset_state(args.state) + failure_count = 0 + + single = (len(args.volume) == 1) + + for volume in args.volume: + try: + utils.find_volume(cs, volume).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for volume %s failed: %s" % (volume, e) + if not single: + print(msg) + + if failure_count == len(args.volume): + if not single: + msg = "Unable to reset the state for any of the specified volumes." + raise exceptions.CommandError(msg) @utils.arg('volume', @@ -556,7 +571,7 @@ def do_snapshot_rename(cs, args): _find_volume_snapshot(cs, args.snapshot).update(**kwargs) -@utils.arg('snapshot', metavar='', +@utils.arg('snapshot', metavar='', nargs='+', help='Name or ID of the snapshot to modify.') @utils.arg('--state', metavar='', default='available', @@ -567,8 +582,24 @@ def do_snapshot_rename(cs, args): @utils.service_type('volumev2') 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) + failure_count = 0 + + single = (len(args.snapshot) == 1) + + for snapshot in args.snapshot: + try: + _find_volume_snapshot(cs, snapshot).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for snapshot %s failed: %s" % (snapshot, e) + if not single: + print(msg) + + if failure_count == len(args.snapshot): + if not single: + msg = ("Unable to reset the state for any of the the specified " + "snapshots.") + raise exceptions.CommandError(msg) def _print_volume_type_list(vtypes): diff --git a/doc/source/index.rst b/doc/source/index.rst index 44cc757..1b9c4f8 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -31,6 +31,8 @@ Release Notes ============= MASTER ------ +* Add support for reset-state on multiple volumes or snapshots at once + .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 From 3f5f9b44d68a471ad354f04bf52f9427685cbf58 Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Mon, 9 Dec 2013 14:25:58 +0000 Subject: [PATCH 140/165] Updates tox.ini to use new features Reasons: - tox update v1.6 Changes: - tox 1.6 allows us to skip the sdist step, which is slow. - It also allows us to override the install line. In this case, it's important as it allows us to stop getting pre-release software we weren't asking for. Original patch by Monty Taylor, talked about here: http://lists.openstack.org/pipermail/openstack-dev/2013-September/015495.html Change-Id: I8a8fdd420eceb5b702e7d9eedd8b5be341d3f56e --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index f4debdd..fcd7d91 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,12 @@ [tox] distribute = False envlist = py26,py27,py33,pypy,pep8 +minversion = 1.6 +skipsdist = True [testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en From c6a85e0c1c8226f5cfbf3dd92b4cd177fa3ee119 Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Mon, 9 Dec 2013 18:07:25 -0500 Subject: [PATCH 141/165] Fix broken argument name in v2 volume extend routine The new_size attribute was misspelled, causing the volume extend operation to fail with: ERROR: 'Namespace' object has no attribute 'new_size' This happens only with the v2 API, v1 works fine. Change-Id: Icb6b5008dad229f21e7906d9b5342d41f5e760cd Closes-Bug: #1259552 --- 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 bce7585..417dd01 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1029,7 +1029,7 @@ def do_transfer_show(cs, args): @utils.arg('volume', metavar='', help='Name or ID of the volume to extend.') -@utils.arg('new-size', +@utils.arg('new_size', metavar='', type=int, help='New size of volume in GB') From 1cfd71c776a1519dbc97886433164a4b69abbe7f Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 10 Dec 2013 00:19:05 +0000 Subject: [PATCH 142/165] Updated from global requirements Change-Id: I53edc66fe4d112ee6f341aa39943630a74c08a87 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a40ca9b..361bda1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 argparse -PrettyTable>=0.6,<0.8 +PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 Babel>=1.3 diff --git a/test-requirements.txt b/test-requirements.txt index a00cd7d..d34074d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,6 @@ discover fixtures>=0.3.14 mock>=1.0 python-subunit -sphinx>=1.1.2 +sphinx>=1.1.2,<1.2 testtools>=0.9.32 testrepository>=0.0.17 From 97f293fea22820e5b2cc1aa3b6ccec76a32ab4fc Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Thu, 14 Nov 2013 18:50:30 +0000 Subject: [PATCH 143/165] Discrepancy between README.rst and cinder help 1) --version in README should be --os-volume-api-version 2) CINDER_URL should be OS_AUTH_URL Change-Id: I409634f3025463f2ae9419ddd0d99fa50554a04d Closes-Bug: 1251385 --- README.rst | 6 +++--- doc/source/index.rst | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 1e86f26..44ea43e 100644 --- a/README.rst +++ b/README.rst @@ -44,13 +44,13 @@ params, but it's easier to just set them as environment variables:: export OS_TENANT_NAME=myproject You will also need to define the authentication url with ``--os-auth-url`` -and the version of the API with ``--version``. Or set them as an environment -variables as well:: +and the version of the API with ``--os-volume-api-version``. Or set them as +environment variables as well:: export OS_AUTH_URL=http://example.com:8774/v1.1/ export OS_VOLUME_API_VERSION=1 -If you are using Keystone, you need to set the CINDER_URL to the keystone +If you are using Keystone, you need to set the OS_AUTH_URL to the keystone endpoint:: export OS_AUTH_URL=http://example.com:5000/v2.0/ diff --git a/doc/source/index.rst b/doc/source/index.rst index 340108d..a16941d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -38,6 +38,7 @@ MASTER .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 .. _1252665: http://bugs.launchpad.net/python-cinderclient/+bug/1252665 .. _1255876: http://bugs.launchpad.net/python-cinderclient/+bug/1255876 +.. _1251385: http://bugs.launchpad.net/python-cinderclient/+bug/1251385 1.0.7 ----- From 420a8fdceaaa09c99b622b36348973ade395062f Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Sat, 14 Dec 2013 16:31:36 +0000 Subject: [PATCH 144/165] Updates .gitignore Remove files we don't generate in the sources. Closes-Bug: #1256043 Closes-Bug: #1257279 Change-Id: I18552182958448baa040c988b02affc231cbf62b --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index ba0f4e3..62527f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ subunit.log *,cover cover *.pyc -.idea -.*.sw? -*~ AUTHORS ChangeLog build From 2e1d92bd6d911be31ccfa1cd920590ecbd0f4d6f Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Thu, 28 Nov 2013 16:56:53 +0000 Subject: [PATCH 145/165] Updates .gitignore for environment files Adding ignore for Eclipse IDE environment files These files .project and .pydevproject should be ignored while pushing to repositories. Change-Id: I44dbdaae71c3ed37c11122f1795d36c401dd431a Closes-Bug: #1256043 --- .gitignore | 4 ++++ doc/source/index.rst | 1 + 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index ba0f4e3..f91dd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ build dist cinderclient/versioninfo python_cinderclient.egg-info + +# Development environment files +.project +.pydevproject diff --git a/doc/source/index.rst b/doc/source/index.rst index a16941d..e1465db 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -33,6 +33,7 @@ MASTER ------ * Add support for reset-state on multiple volumes or snapshots at once +.. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 From 12ee7b72ff823df92e06df847c2f5c3e2dc152b3 Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Fri, 27 Dec 2013 10:32:11 +0800 Subject: [PATCH 146/165] Fix inappropriate comment for volume update api Change-Id: Id7a436cc864772bd2662fce0ef80d647c9e3dcf0 Closes-Bug: #1264415 --- cinderclient/v1/volumes.py | 2 +- doc/source/index.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py index a74c835..238e29f 100644 --- a/cinderclient/v1/volumes.py +++ b/cinderclient/v1/volumes.py @@ -231,7 +231,7 @@ class VolumeManager(base.ManagerWithFind): """ Update the display_name or display_description for a volume. - :param volume: The :class:`Volume` to delete. + :param volume: The :class:`Volume` to update. """ if not kwargs: return diff --git a/doc/source/index.rst b/doc/source/index.rst index a16941d..3da3395 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -39,6 +39,7 @@ MASTER .. _1252665: http://bugs.launchpad.net/python-cinderclient/+bug/1252665 .. _1255876: http://bugs.launchpad.net/python-cinderclient/+bug/1255876 .. _1251385: http://bugs.launchpad.net/python-cinderclient/+bug/1251385 +.. _1264415: http://bugs.launchpad.net/python-cinderclient/+bug/1264415 1.0.7 ----- From 1fc1b4bea1f813ec54fc448aad3794ca78507a11 Mon Sep 17 00:00:00 2001 From: "Igor A. Lukyanenkov" Date: Tue, 3 Dec 2013 15:47:58 +0400 Subject: [PATCH 147/165] Fix RateLimit.__repr__ - self.method is undefined Using self.verb instead of self.method, unit tests added to cover this fix. Closes-bug: #1258489 Change-Id: I4a84179ff96c50b230afcaea7a19fb54613577fb --- cinderclient/tests/v1/test_limits.py | 24 ++++++++++++++++++++++++ cinderclient/tests/v2/test_limits.py | 24 ++++++++++++++++++++++++ cinderclient/v1/limits.py | 2 +- cinderclient/v2/limits.py | 2 +- doc/source/index.rst | 1 + 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 cinderclient/tests/v1/test_limits.py create mode 100644 cinderclient/tests/v2/test_limits.py diff --git a/cinderclient/tests/v1/test_limits.py b/cinderclient/tests/v1/test_limits.py new file mode 100644 index 0000000..51420f4 --- /dev/null +++ b/cinderclient/tests/v1/test_limits.py @@ -0,0 +1,24 @@ +# 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.tests import utils +from cinderclient.v1 import limits + + +class TestRateLimit(utils.TestCase): + def test_repr(self): + l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1", + "unit1", "next1") + self.assertEqual("", repr(l1)) diff --git a/cinderclient/tests/v2/test_limits.py b/cinderclient/tests/v2/test_limits.py new file mode 100644 index 0000000..36f3ace --- /dev/null +++ b/cinderclient/tests/v2/test_limits.py @@ -0,0 +1,24 @@ +# 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.tests import utils +from cinderclient.v2 import limits + + +class TestRateLimit(utils.TestCase): + def test_repr(self): + l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1", + "unit1", "next1") + self.assertEqual("", repr(l1)) diff --git a/cinderclient/v1/limits.py b/cinderclient/v1/limits.py index 32421e8..1ae2815 100644 --- a/cinderclient/v1/limits.py +++ b/cinderclient/v1/limits.py @@ -61,7 +61,7 @@ class RateLimit(object): and self.next_available == other.next_available def __repr__(self): - return "" % (self.method, self.uri) + return "" % (self.verb, self.uri) class AbsoluteLimit(object): diff --git a/cinderclient/v2/limits.py b/cinderclient/v2/limits.py index 16eb515..512a58d 100644 --- a/cinderclient/v2/limits.py +++ b/cinderclient/v2/limits.py @@ -61,7 +61,7 @@ class RateLimit(object): and self.next_available == other.next_available def __repr__(self): - return "" % (self.method, self.uri) + return "" % (self.verb, self.uri) class AbsoluteLimit(object): diff --git a/doc/source/index.rst b/doc/source/index.rst index 3da3395..fd0b459 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -40,6 +40,7 @@ MASTER .. _1255876: http://bugs.launchpad.net/python-cinderclient/+bug/1255876 .. _1251385: http://bugs.launchpad.net/python-cinderclient/+bug/1251385 .. _1264415: http://bugs.launchpad.net/python-cinderclient/+bug/1264415 +.. _1258489: http://bugs.launchpad.net/python-cinderclient/+bug/1258489 1.0.7 ----- From 47014971b09f0334e5cc3b414bef31784f88a37d Mon Sep 17 00:00:00 2001 From: Swapnil Kulkarni Date: Wed, 8 Jan 2014 13:24:06 +0530 Subject: [PATCH 148/165] Update cinderclient to skip the additional GET during create With change https://review.openstack.org/64945 cinder V2 API now returns volume details. Updated do_create in cinderclient to remove additional GET call. Change-Id: I2a36f2fa865b52f228bad44e0a9c67d49228321e Partial-Bug: #1265893 --- cinderclient/v2/shell.py | 1 - doc/source/index.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index e835486..0182f32 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -296,7 +296,6 @@ def do_create(cs, args): scheduler_hints=hints) info = dict() - volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links', None) diff --git a/doc/source/index.rst b/doc/source/index.rst index 4f00787..8475660 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -33,6 +33,7 @@ MASTER ------ * Add support for reset-state on multiple volumes or snapshots at once +.. _1265893: https://bugs.launchpad.net/python-cinderclient/+bug/1265893 .. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 From efc080dfcaae6ce0381e3e1a94f9efca350eac8a Mon Sep 17 00:00:00 2001 From: "Igor A. Lukyanenkov" Date: Tue, 3 Dec 2013 17:50:19 +0400 Subject: [PATCH 149/165] Unit tests for limits.py in cinderclient/v1 and v2 Change-Id: I962161ac657d9ec676f3b03ab17138ed1911ec35 --- cinderclient/tests/v1/test_limits.py | 144 ++++++++++++++++++++++++++- cinderclient/tests/v2/test_limits.py | 144 ++++++++++++++++++++++++++- 2 files changed, 284 insertions(+), 4 deletions(-) diff --git a/cinderclient/tests/v1/test_limits.py b/cinderclient/tests/v1/test_limits.py index 51420f4..b4520e3 100644 --- a/cinderclient/tests/v1/test_limits.py +++ b/cinderclient/tests/v1/test_limits.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack Foundation +# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,12 +13,152 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock + from cinderclient.tests import utils from cinderclient.v1 import limits -class TestRateLimit(utils.TestCase): +def _get_default_RateLimit(verb="verb1", uri="uri1", regex="regex1", + value="value1", + remain="remain1", unit="unit1", + next_available="next1"): + return limits.RateLimit(verb, uri, regex, value, remain, unit, + next_available) + + +class TestLimits(utils.TestCase): def test_repr(self): + l = limits.Limits(None, {"foo": "bar"}) + self.assertEqual("", repr(l)) + + def test_absolute(self): + l = limits.Limits(None, + {"absolute": {"name1": "value1", "name2": "value2"}}) + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name2", "value2") + for item in l.absolute: + self.assertIn(item, [l1, l2]) + + def test_rate(self): + l = limits.Limits(None, + { + "rate": [ + { + "uri": "uri1", + "regex": "regex1", + "limit": [ + { + "verb": "verb1", + "value": "value1", + "remaining": "remain1", + "unit": "unit1", + "next-available": "next1", + }, + ], + }, + { + "uri": "uri2", + "regex": "regex2", + "limit": [ + { + "verb": "verb2", + "value": "value2", + "remaining": "remain2", + "unit": "unit2", + "next-available": "next2", + }, + ], + }, + ], + }) l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1", "unit1", "next1") + l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2", + "unit2", "next2") + for item in l.rate: + self.assertTrue(item in [l1, l2]) + + +class TestRateLimit(utils.TestCase): + def test_equal(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit() + self.assertTrue(l1 == l2) + + def test_not_equal_verbs(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(verb="verb2") + self.assertFalse(l1 == l2) + + def test_not_equal_uris(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(uri="uri2") + self.assertFalse(l1 == l2) + + def test_not_equal_regexps(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(regex="regex2") + self.assertFalse(l1 == l2) + + def test_not_equal_values(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(value="value2") + self.assertFalse(l1 == l2) + + def test_not_equal_remains(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(remain="remain2") + self.assertFalse(l1 == l2) + + def test_not_equal_units(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(unit="unit2") + self.assertFalse(l1 == l2) + + def test_not_equal_next_available(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(next_available="next2") + self.assertFalse(l1 == l2) + + def test_repr(self): + l1 = _get_default_RateLimit() self.assertEqual("", repr(l1)) + + +class TestAbsoluteLimit(utils.TestCase): + def test_equal(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name1", "value1") + self.assertTrue(l1 == l2) + + def test_not_equal_values(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name1", "value2") + self.assertFalse(l1 == l2) + + def test_not_equal_names(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name2", "value1") + self.assertFalse(l1 == l2) + + def test_repr(self): + l1 = limits.AbsoluteLimit("name1", "value1") + self.assertEqual("", repr(l1)) + + +class TestLimitsManager(utils.TestCase): + def test_get(self): + api = mock.Mock() + api.client.get.return_value = ( + None, + {"limits": {"absolute": {"name1": "value1", }}, + "no-limits": {"absolute": {"name2": "value2", }}}) + l1 = limits.AbsoluteLimit("name1", "value1") + limitsManager = limits.LimitsManager(api) + + lim = limitsManager.get() + + self.assertIsInstance(lim, limits.Limits) + for l in lim.absolute: + self.assertEqual(l, l1) diff --git a/cinderclient/tests/v2/test_limits.py b/cinderclient/tests/v2/test_limits.py index 36f3ace..92b50cd 100644 --- a/cinderclient/tests/v2/test_limits.py +++ b/cinderclient/tests/v2/test_limits.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack Foundation +# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,12 +13,152 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock + from cinderclient.tests import utils from cinderclient.v2 import limits -class TestRateLimit(utils.TestCase): +def _get_default_RateLimit(verb="verb1", uri="uri1", regex="regex1", + value="value1", + remain="remain1", unit="unit1", + next_available="next1"): + return limits.RateLimit(verb, uri, regex, value, remain, unit, + next_available) + + +class TestLimits(utils.TestCase): def test_repr(self): + l = limits.Limits(None, {"foo": "bar"}) + self.assertEqual("", repr(l)) + + def test_absolute(self): + l = limits.Limits(None, + {"absolute": {"name1": "value1", "name2": "value2"}}) + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name2", "value2") + for item in l.absolute: + self.assertIn(item, [l1, l2]) + + def test_rate(self): + l = limits.Limits(None, + { + "rate": [ + { + "uri": "uri1", + "regex": "regex1", + "limit": [ + { + "verb": "verb1", + "value": "value1", + "remaining": "remain1", + "unit": "unit1", + "next-available": "next1", + }, + ], + }, + { + "uri": "uri2", + "regex": "regex2", + "limit": [ + { + "verb": "verb2", + "value": "value2", + "remaining": "remain2", + "unit": "unit2", + "next-available": "next2", + }, + ], + }, + ], + }) l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1", "unit1", "next1") + l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2", + "unit2", "next2") + for item in l.rate: + self.assertTrue(item in [l1, l2]) + + +class TestRateLimit(utils.TestCase): + def test_equal(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit() + self.assertTrue(l1 == l2) + + def test_not_equal_verbs(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(verb="verb2") + self.assertFalse(l1 == l2) + + def test_not_equal_uris(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(uri="uri2") + self.assertFalse(l1 == l2) + + def test_not_equal_regexps(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(regex="regex2") + self.assertFalse(l1 == l2) + + def test_not_equal_values(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(value="value2") + self.assertFalse(l1 == l2) + + def test_not_equal_remains(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(remain="remain2") + self.assertFalse(l1 == l2) + + def test_not_equal_units(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(unit="unit2") + self.assertFalse(l1 == l2) + + def test_not_equal_next_available(self): + l1 = _get_default_RateLimit() + l2 = _get_default_RateLimit(next_available="next2") + self.assertFalse(l1 == l2) + + def test_repr(self): + l1 = _get_default_RateLimit() self.assertEqual("", repr(l1)) + + +class TestAbsoluteLimit(utils.TestCase): + def test_equal(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name1", "value1") + self.assertTrue(l1 == l2) + + def test_not_equal_values(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name1", "value2") + self.assertFalse(l1 == l2) + + def test_not_equal_names(self): + l1 = limits.AbsoluteLimit("name1", "value1") + l2 = limits.AbsoluteLimit("name2", "value1") + self.assertFalse(l1 == l2) + + def test_repr(self): + l1 = limits.AbsoluteLimit("name1", "value1") + self.assertEqual("", repr(l1)) + + +class TestLimitsManager(utils.TestCase): + def test_get(self): + api = mock.Mock() + api.client.get.return_value = ( + None, + {"limits": {"absolute": {"name1": "value1", }}, + "no-limits": {"absolute": {"name2": "value2", }}}) + l1 = limits.AbsoluteLimit("name1", "value1") + limitsManager = limits.LimitsManager(api) + + lim = limitsManager.get() + + self.assertIsInstance(lim, limits.Limits) + for l in lim.absolute: + self.assertEqual(l, l1) From 69188a95513c6bf12c281157d8cffb4e00c04eaa Mon Sep 17 00:00:00 2001 From: Alexander Ignatov Date: Thu, 16 Jan 2014 02:12:43 +0400 Subject: [PATCH 150/165] Remove copyright from empty files According to policy change in HACKING: http://docs.openstack.org/developer/hacking/#openstack-licensing empty files should no longer contain copyright notices. Change-Id: I357adea1d295cdc11a12194ad7116d73a8560122 Closes-Bug: #1262424 --- cinderclient/tests/v2/__init__.py | 15 --------------- cinderclient/v1/contrib/__init__.py | 14 -------------- cinderclient/v2/contrib/__init__.py | 15 --------------- 3 files changed, 44 deletions(-) diff --git a/cinderclient/tests/v2/__init__.py b/cinderclient/tests/v2/__init__.py index f2c41f4..e69de29 100644 --- a/cinderclient/tests/v2/__init__.py +++ b/cinderclient/tests/v2/__init__.py @@ -1,15 +0,0 @@ -# Copyright (c) 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/v1/contrib/__init__.py b/cinderclient/v1/contrib/__init__.py index 788cea1..e69de29 100644 --- a/cinderclient/v1/contrib/__init__.py +++ b/cinderclient/v1/contrib/__init__.py @@ -1,14 +0,0 @@ -# Copyright (c) 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/v2/contrib/__init__.py b/cinderclient/v2/contrib/__init__.py index f2c41f4..e69de29 100644 --- a/cinderclient/v2/contrib/__init__.py +++ b/cinderclient/v2/contrib/__init__.py @@ -1,15 +0,0 @@ -# Copyright (c) 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. From a1a36499f428781c1966db7090001128a6abd888 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 16 Jan 2014 13:19:28 +0100 Subject: [PATCH 151/165] Remove dependencies on pep8, pyflakes and flake8 They should be determined by the hacking dependency implicitely. Change-Id: I5c077e4b8e5985e86af3229a58aa475ac668b5e1 --- test-requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index d34074d..0c60d32 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,4 @@ -# Install bounded pep8/pyflakes first, then let flake8 install -pep8==1.4.5 -pyflakes>=0.7.2,<0.7.4 -flake8==2.0 +# Hacking already pins down pep8, pyflakes and flake8 hacking>=0.8.0,<0.9 coverage>=3.6 discover From 2f0863b7d4d20198f496534d5ffef85e68ff45fc Mon Sep 17 00:00:00 2001 From: Xiao Chen Date: Mon, 20 Jan 2014 15:29:55 +0800 Subject: [PATCH 152/165] Fix 'search_opts' unexpected keyword argument error When executing 'encryption-type-create' command using resource name, it will use the findall() in base.py, then error will occur because list() in findall() used 'search_opts' argument which list() in VolumeEncryptionTypeManager did not have. Some other command like 'transfer-delete' met same issue. This patch will add 'search_opts' argument in list() function in child class to avoid the bug. Closes-Bug: #1248519 Related-Bug: #1252665 Change-Id: If1cd8b812af21335e5d4351766539a927a8fbdf4 --- cinderclient/v1/volume_encryption_types.py | 2 +- cinderclient/v1/volume_transfers.py | 2 +- cinderclient/v2/volume_encryption_types.py | 2 +- cinderclient/v2/volume_transfers.py | 2 +- doc/source/index.rst | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cinderclient/v1/volume_encryption_types.py b/cinderclient/v1/volume_encryption_types.py index b97c6f0..60d2f47 100644 --- a/cinderclient/v1/volume_encryption_types.py +++ b/cinderclient/v1/volume_encryption_types.py @@ -36,7 +36,7 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind): """ resource_class = VolumeEncryptionType - def list(self): + def list(self, search_opts=None): """ List all volume encryption types. diff --git a/cinderclient/v1/volume_transfers.py b/cinderclient/v1/volume_transfers.py index 8aace02..e6b0e30 100644 --- a/cinderclient/v1/volume_transfers.py +++ b/cinderclient/v1/volume_transfers.py @@ -64,7 +64,7 @@ class VolumeTransferManager(base.ManagerWithFind): """ return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") - def list(self, detailed=True): + def list(self, detailed=True, search_opts=None): """Get a list of all volume transfer. :rtype: list of :class:`VolumeTransfer` diff --git a/cinderclient/v2/volume_encryption_types.py b/cinderclient/v2/volume_encryption_types.py index b97c6f0..60d2f47 100644 --- a/cinderclient/v2/volume_encryption_types.py +++ b/cinderclient/v2/volume_encryption_types.py @@ -36,7 +36,7 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind): """ resource_class = VolumeEncryptionType - def list(self): + def list(self, search_opts=None): """ List all volume encryption types. diff --git a/cinderclient/v2/volume_transfers.py b/cinderclient/v2/volume_transfers.py index 8aace02..e6b0e30 100644 --- a/cinderclient/v2/volume_transfers.py +++ b/cinderclient/v2/volume_transfers.py @@ -64,7 +64,7 @@ class VolumeTransferManager(base.ManagerWithFind): """ return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") - def list(self, detailed=True): + def list(self, detailed=True, search_opts=None): """Get a list of all volume transfer. :rtype: list of :class:`VolumeTransfer` diff --git a/doc/source/index.rst b/doc/source/index.rst index 8475660..5f40bb1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -43,6 +43,7 @@ MASTER .. _1251385: http://bugs.launchpad.net/python-cinderclient/+bug/1251385 .. _1264415: http://bugs.launchpad.net/python-cinderclient/+bug/1264415 .. _1258489: http://bugs.launchpad.net/python-cinderclient/+bug/1258489 +.. _1248519: http://bugs.launchpad.net/python-cinderclient/+bug/1248519 1.0.7 ----- From c2ef5eaf494401cb2e6a77c516d77fbe06bc6f08 Mon Sep 17 00:00:00 2001 From: Jay Lau Date: Tue, 21 Jan 2014 22:55:34 +0800 Subject: [PATCH 153/165] disable/enable a service has no output When disable/enable a cinder service, there is no output, this caused end user does not know if the service was disabled/enabled successfully. It is better add some output for the command just like nova to tell end user the service status. Change-Id: I9d440ab7437c268b17627e255f152db0b3bf4f53 Closes-Bug: #1261120 --- cinderclient/tests/v1/fakes.py | 4 ++-- cinderclient/tests/v1/test_services.py | 8 ++++++-- cinderclient/tests/v1/test_shell.py | 10 ++++++++++ cinderclient/tests/v2/fakes.py | 4 ++-- cinderclient/tests/v2/test_services.py | 8 ++++++-- cinderclient/tests/v2/test_shell.py | 10 ++++++++++ cinderclient/v1/services.py | 6 ++++-- cinderclient/v1/shell.py | 8 ++++++-- cinderclient/v2/services.py | 6 ++++-- cinderclient/v2/shell.py | 8 ++++++-- 10 files changed, 56 insertions(+), 16 deletions(-) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 1bebecb..0bd11c8 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -696,11 +696,11 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_enable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], - 'status': 'disabled'}) + 'status': 'enabled'}) def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], - 'status': 'enabled'}) + 'status': 'disabled'}) def get_os_availability_zone(self, **kw): return (200, {}, { diff --git a/cinderclient/tests/v1/test_services.py b/cinderclient/tests/v1/test_services.py index 7a1ec85..de89935 100644 --- a/cinderclient/tests/v1/test_services.py +++ b/cinderclient/tests/v1/test_services.py @@ -52,11 +52,15 @@ class ServicesTest(utils.TestCase): [self.assertEqual(s.binary, 'cinder-volume') for s in svs] def test_services_enable(self): - cs.services.enable('host1', 'cinder-volume') + s = cs.services.enable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/enable', values) + self.assertTrue(isinstance(s, services.Service)) + self.assertEqual(s.status, 'enabled') def test_services_disable(self): - cs.services.disable('host1', 'cinder-volume') + s = cs.services.disable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/disable', values) + self.assertTrue(isinstance(s, services.Service)) + self.assertEqual(s.status, 'disabled') diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index a679e4c..5899d5e 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -335,3 +335,13 @@ class ShellTest(utils.TestCase): self.run_command('readonly-mode-update 1234 False') expected = {'os-update_readonly_flag': {'readonly': False}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_service_disable(self): + self.run_command('service-disable host cinder-volume') + self.assert_called('PUT', '/os-services/disable', + {"binary": "cinder-volume", "host": "host"}) + + def test_service_disable(self): + self.run_command('service-enable host cinder-volume') + self.assert_called('PUT', '/os-services/enable', + {"binary": "cinder-volume", "host": "host"}) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 01b9404..784791a 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -706,11 +706,11 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_enable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], - 'status': 'disabled'}) + 'status': 'enabled'}) def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], - 'status': 'enabled'}) + 'status': 'disabled'}) def get_os_availability_zone(self, **kw): return (200, {}, { diff --git a/cinderclient/tests/v2/test_services.py b/cinderclient/tests/v2/test_services.py index 5ee3ea1..07a2389 100644 --- a/cinderclient/tests/v2/test_services.py +++ b/cinderclient/tests/v2/test_services.py @@ -52,11 +52,15 @@ class ServicesTest(utils.TestCase): [self.assertEqual(s.binary, 'cinder-volume') for s in svs] def test_services_enable(self): - cs.services.enable('host1', 'cinder-volume') + s = cs.services.enable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/enable', values) + self.assertTrue(isinstance(s, services.Service)) + self.assertEqual(s.status, 'enabled') def test_services_disable(self): - cs.services.disable('host1', 'cinder-volume') + s = cs.services.disable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/disable', values) + self.assertTrue(isinstance(s, services.Service)) + self.assertEqual(s.status, 'disabled') diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index eccfef9..b84cfac 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -313,3 +313,13 @@ class ShellTest(utils.TestCase): self.run_command('readonly-mode-update 1234 False') expected = {'os-update_readonly_flag': {'readonly': False}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_service_disable(self): + self.run_command('service-disable host cinder-volume') + self.assert_called('PUT', '/os-services/disable', + {"binary": "cinder-volume", "host": "host"}) + + def test_service_disable(self): + self.run_command('service-enable host cinder-volume') + self.assert_called('PUT', '/os-services/enable', + {"binary": "cinder-volume", "host": "host"}) diff --git a/cinderclient/v1/services.py b/cinderclient/v1/services.py index 6afd5c5..2669a5e 100644 --- a/cinderclient/v1/services.py +++ b/cinderclient/v1/services.py @@ -48,9 +48,11 @@ class ServiceManager(base.ManagerWithFind): def enable(self, host, binary): """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} - self._update("/os-services/enable", body) + result = self._update("/os-services/enable", body) + return self.resource_class(self, result) def disable(self, host, binary): """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} - self._update("/os-services/disable", body) + result = self._update("/os-services/disable", body) + return self.resource_class(self, result) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index acefe26..f916850 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -993,7 +993,9 @@ def do_service_list(cs, args): @utils.service_type('volume') def do_service_enable(cs, args): """Enable the service.""" - cs.services.enable(args.host, args.binary) + result = cs.services.enable(args.host, args.binary) + columns = ["Host", "Binary", "Status"] + utils.print_list([result], columns) @utils.arg('host', metavar='', help='Name of host.') @@ -1001,7 +1003,9 @@ def do_service_enable(cs, args): @utils.service_type('volume') def do_service_disable(cs, args): """Disable the service.""" - cs.services.disable(args.host, args.binary) + result = cs.services.disable(args.host, args.binary) + columns = ["Host", "Binary", "Status"] + utils.print_list([result], columns) def _treeizeAvailabilityZone(zone): diff --git a/cinderclient/v2/services.py b/cinderclient/v2/services.py index 6afd5c5..2669a5e 100644 --- a/cinderclient/v2/services.py +++ b/cinderclient/v2/services.py @@ -48,9 +48,11 @@ class ServiceManager(base.ManagerWithFind): def enable(self, host, binary): """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} - self._update("/os-services/enable", body) + result = self._update("/os-services/enable", body) + return self.resource_class(self, result) def disable(self, host, binary): """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} - self._update("/os-services/disable", body) + result = self._update("/os-services/disable", body) + return self.resource_class(self, result) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 0182f32..a583c69 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -1087,7 +1087,9 @@ def do_service_list(cs, args): @utils.service_type('volumev2') def do_service_enable(cs, args): """Enable the service.""" - cs.services.enable(args.host, args.binary) + result = cs.services.enable(args.host, args.binary) + columns = ["Host", "Binary", "Status"] + utils.print_list([result], columns) @utils.arg('host', metavar='', help='Name of host.') @@ -1095,7 +1097,9 @@ def do_service_enable(cs, args): @utils.service_type('volumev2') def do_service_disable(cs, args): """Disable the service.""" - cs.services.disable(args.host, args.binary) + result = cs.services.disable(args.host, args.binary) + columns = ["Host", "Binary", "Status"] + utils.print_list([result], columns) def _treeizeAvailabilityZone(zone): From fbee97cf196fa4517c753f2daadb3606d3b5dca3 Mon Sep 17 00:00:00 2001 From: Swapnil Kulkarni Date: Sat, 25 Jan 2014 08:13:25 +0530 Subject: [PATCH 154/165] Remove RAX-specific auth in cinderclient Change-Id: Idd36694a322f467ba5e8c1f58bfac701bff8f3f8 Closes-Bug: #966329 --- cinderclient/client.py | 16 +--------------- doc/source/index.rst | 1 + 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/cinderclient/client.py b/cinderclient/client.py index a9f11f1..c268137 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -22,7 +22,6 @@ OpenStack Client interface. Handles the REST calls and responses. from __future__ import print_function import logging -import os try: import urlparse @@ -296,10 +295,7 @@ class HTTPClient(object): auth_url = self.auth_url if self.version == "v2.0": while auth_url: - if "CINDER_RAX_AUTH" in os.environ: - auth_url = self._rax_auth(auth_url) - else: - auth_url = self._v2_auth(auth_url) + auth_url = self._v2_auth(auth_url) # Are we acting on behalf of another user via an # existing token? If so, our actual endpoints may @@ -358,16 +354,6 @@ class HTTPClient(object): self._authenticate(url, body) - def _rax_auth(self, url): - """Authenticate against the Rackspace auth service.""" - body = {"auth": { - "RAX-KSKEY:apiKeyCredentials": { - "username": self.user, - "apiKey": self.password, - "tenantName": self.projectid}}} - - self._authenticate(url, body) - def _authenticate(self, url, body): """Authenticate and extract the service catalog.""" token_url = url + "/tokens" diff --git a/doc/source/index.rst b/doc/source/index.rst index 8475660..73b55aa 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -33,6 +33,7 @@ MASTER ------ * Add support for reset-state on multiple volumes or snapshots at once +.. _966329: https://bugs.launchpad.net/python-cinderclient/+bug/966329 .. _1265893: https://bugs.launchpad.net/python-cinderclient/+bug/1265893 .. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 From 58d8a00a5722d275deaff7625781c5cad11b53ed Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Wed, 18 Sep 2013 13:21:00 +0300 Subject: [PATCH 155/165] Add volume retype command Add volume retype to Cinder client. Cinder code: https://review.openstack.org/#/c/44881/ Change-Id: Ied4902da531386f744403962e4a8ddfb7587c4ef --- cinderclient/tests/v2/fakes.py | 2 ++ cinderclient/tests/v2/test_shell.py | 14 +++++++++++++- cinderclient/tests/v2/test_volumes.py | 7 +++++++ cinderclient/v2/shell.py | 13 +++++++++++++ cinderclient/v2/volumes.py | 19 +++++++++++++++---- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 70c5840..086034a 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -360,6 +360,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert 'force_host_copy' in body[action] elif action == 'os-update_readonly_flag': assert list(body[action]) == ['readonly'] + elif action == 'os-retype': + assert 'new_type' in body[action] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index d203c07..85a230a 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -325,7 +325,19 @@ class ShellTest(utils.TestCase): self.assert_called('PUT', '/os-services/disable', {"binary": "cinder-volume", "host": "host"}) - def test_service_disable(self): + def test_service_enable(self): self.run_command('service-enable host cinder-volume') self.assert_called('PUT', '/os-services/enable', {"binary": "cinder-volume", "host": "host"}) + + def test_retype_with_policy(self): + self.run_command('retype 1234 foo --migration-policy=on-demand') + expected = {'os-retype': {'new_type': 'foo', + 'migration_policy': 'on-demand'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_retype_default_policy(self): + self.run_command('retype 1234 foo') + expected = {'os-retype': {'new_type': 'foo', + 'migration_policy': 'never'}} + self.assert_called('POST', '/volumes/1234/action', body=expected) diff --git a/cinderclient/tests/v2/test_volumes.py b/cinderclient/tests/v2/test_volumes.py index 0ec345c..5e12812 100644 --- a/cinderclient/tests/v2/test_volumes.py +++ b/cinderclient/tests/v2/test_volumes.py @@ -109,3 +109,10 @@ class VolumesTest(utils.TestCase): v = cs.volumes.get('1234') cs.volumes.update_readonly_flag(v, True) cs.assert_called('POST', '/volumes/1234/action') + + def test_retype(self): + v = cs.volumes.get('1234') + cs.volumes.retype(v, 'foo', 'on-demand') + cs.assert_called('POST', '/volumes/1234/action', + {'os-retype': {'new_type': 'foo', + 'migration_policy': 'on-demand'}}) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 55ceb5b..5c1e86f 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -899,6 +899,19 @@ def do_migrate(cs, args): volume.migrate_volume(args.host, args.force_host_copy) +@utils.arg('volume', metavar='', + help='Name or ID of the volume to retype') +@utils.arg('new_type', metavar='', help='New volume type') +@utils.arg('--migration-policy', metavar='', required=False, + choices=['never', 'on-demand'], default='never', + help='Policy on migrating the volume during the retype.') +@utils.service_type('volumev2') +def do_retype(cs, args): + """Change the volume's type.""" + volume = utils.find_volume(cs, args.volume) + volume.retype(args.new_type, args.migration_policy) + + @utils.arg('volume', metavar='', help='Name or ID of the volume to backup.') @utils.arg('--container', metavar='', diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index 4f4a2bb..524456d 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -117,10 +117,9 @@ class Volume(base.Resource): """Migrate the volume to a new host.""" self.manager.migrate_volume(self, host, force_host_copy) -# def migrate_volume_completion(self, old_volume, new_volume, error): -# """Complete the migration of the volume.""" -# self.manager.migrate_volume_completion(self, old_volume, -# new_volume, error) + def retype(self, volume_type, policy): + """Change a volume's type.""" + self.manager.retype(self, volume_type, policy) def update_all_metadata(self, metadata): """Update all metadata of this volume.""" @@ -408,3 +407,15 @@ class VolumeManager(base.ManagerWithFind): return self._action('os-update_readonly_flag', base.getid(volume), {'readonly': flag}) + + def retype(self, volume, volume_type, policy): + """Change a volume's type. + + :param volume: The :class:`Volume` to retype + :param volume_type: New volume type + :param policy: Policy for migration during the retype + """ + return self._action('os-retype', + volume, + {'new_type': volume_type, + 'migration_policy': policy}) From 17d7455d27ce2f98a99b106696a37270931d3de8 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Sun, 26 Jan 2014 09:28:48 +0200 Subject: [PATCH 156/165] Add retype to index.rst Retype patch was merged but not added to index.rst Change-Id: I15e52153fccf07026d4b1dfcc1fb62a27d6d3ca2 --- doc/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 5f40bb1..e7ca6fd 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -32,6 +32,7 @@ Release Notes MASTER ------ * Add support for reset-state on multiple volumes or snapshots at once +* Add volume retype command .. _1265893: https://bugs.launchpad.net/python-cinderclient/+bug/1265893 .. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 From c5b5646f9e7983649a04f4ea38f5040cff1041f6 Mon Sep 17 00:00:00 2001 From: Swapnil Kulkarni Date: Sun, 26 Jan 2014 15:42:05 +0530 Subject: [PATCH 157/165] Sync up with oslo-incubator be81d6b Cleanup unused log related code e53fe85 strutils bool_from_string, allow specified default 12bcdb7 Remove vim header 12d3bbc Add method quote_plus in module py3kcompat.urlutils afdbc0a Fix E501 in individual openstack projects Oslo Version: 8dee4b6 Merge "remove extra newlines that eventlet seems to add" Thu, 23 Jan 2014 14:26:42 +0530 (08:56 +0000) Change-Id: I7348e797a4a78ec20dff77ebdbb41bb8c6068b34 --- .../openstack/common/apiclient/__init__.py | 16 - .../openstack/common/apiclient/auth.py | 16 +- .../openstack/common/apiclient/base.py | 17 +- .../openstack/common/apiclient/client.py | 4 +- .../openstack/common/apiclient/exceptions.py | 30 +- .../openstack/common/apiclient/fake_client.py | 9 +- cinderclient/openstack/common/gettextutils.py | 449 ++++++++++-------- cinderclient/openstack/common/importutils.py | 2 - .../openstack/common/py3kcompat/__init__.py | 0 .../openstack/common/py3kcompat/urlutils.py | 67 +++ cinderclient/openstack/common/strutils.py | 24 +- tools/install_venv_common.py | 41 -- 12 files changed, 376 insertions(+), 299 deletions(-) create mode 100644 cinderclient/openstack/common/py3kcompat/__init__.py create mode 100644 cinderclient/openstack/common/py3kcompat/urlutils.py diff --git a/cinderclient/openstack/common/apiclient/__init__.py b/cinderclient/openstack/common/apiclient/__init__.py index d5d0022..e69de29 100644 --- a/cinderclient/openstack/common/apiclient/__init__.py +++ b/cinderclient/openstack/common/apiclient/__init__.py @@ -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/auth.py b/cinderclient/openstack/common/apiclient/auth.py index 374d20b..1a713b0 100644 --- a/cinderclient/openstack/common/apiclient/auth.py +++ b/cinderclient/openstack/common/apiclient/auth.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 OpenStack Foundation # Copyright 2013 Spanish National Research Council. # All Rights Reserved. @@ -21,17 +19,14 @@ import abc import argparse -import logging import os +import six from stevedore import extension from cinderclient.openstack.common.apiclient import exceptions -logger = logging.getLogger(__name__) - - _discovered_plugins = {} @@ -59,7 +54,7 @@ def load_auth_system_opts(parser): """ group = parser.add_argument_group("Common auth options") BaseAuthPlugin.add_common_opts(group) - for name, auth_plugin in _discovered_plugins.iteritems(): + for name, auth_plugin in six.iteritems(_discovered_plugins): group = parser.add_argument_group( "Auth-system '%s' options" % name, conflict_handler="resolve") @@ -75,7 +70,7 @@ def load_plugin(auth_system): def load_plugin_from_args(args): - """Load requred plugin and populate it with options. + """Load required plugin and populate it with options. Try to guess auth system if it is not specified. Systems are tried in alphabetical order. @@ -90,7 +85,7 @@ def load_plugin_from_args(args): plugin.sufficient_options() return plugin - for plugin_auth_system in sorted(_discovered_plugins.iterkeys()): + for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): plugin_class = _discovered_plugins[plugin_auth_system] plugin = plugin_class() plugin.parse_opts(args) @@ -102,6 +97,7 @@ def load_plugin_from_args(args): raise exceptions.AuthPluginOptionsMissing(["auth_system"]) +@six.add_metaclass(abc.ABCMeta) class BaseAuthPlugin(object): """Base class for authentication plugins. @@ -109,8 +105,6 @@ class BaseAuthPlugin(object): method to be a valid plugin. """ - __metaclass__ = abc.ABCMeta - auth_system = None opt_names = [] common_opt_names = [ diff --git a/cinderclient/openstack/common/apiclient/base.py b/cinderclient/openstack/common/apiclient/base.py index caef843..e101f4c 100644 --- a/cinderclient/openstack/common/apiclient/base.py +++ b/cinderclient/openstack/common/apiclient/base.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics @@ -26,9 +24,11 @@ Base utilities to build API operation managers and objects on top of. # pylint: disable=E1102 import abc -import urllib + +import six from cinderclient.openstack.common.apiclient import exceptions +from cinderclient.openstack.common.py3kcompat import urlutils from cinderclient.openstack.common import strutils @@ -201,11 +201,10 @@ class BaseManager(HookableMixin): return self.client.delete(url) +@six.add_metaclass(abc.ABCMeta) class ManagerWithFind(BaseManager): """Manager with additional `find()`/`findall()` methods.""" - __metaclass__ = abc.ABCMeta - @abc.abstractmethod def list(self): pass @@ -292,7 +291,7 @@ class CrudManager(BaseManager): def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" - for key, ref in kwargs.copy().iteritems(): + for key, ref in six.iteritems(kwargs.copy()): if ref is None: kwargs.pop(key) else: @@ -328,7 +327,7 @@ class CrudManager(BaseManager): return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', }, self.collection_key) @@ -367,7 +366,7 @@ class CrudManager(BaseManager): rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) @@ -446,7 +445,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) self._info[k] = v diff --git a/cinderclient/openstack/common/apiclient/client.py b/cinderclient/openstack/common/apiclient/client.py index 77d4579..da2e177 100644 --- a/cinderclient/openstack/common/apiclient/client.py +++ b/cinderclient/openstack/common/apiclient/client.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. @@ -52,7 +50,7 @@ class HTTPClient(object): services (e.g., for compute and image clients); - reissue authentication request for expired tokens; - encode/decode JSON bodies; - - raise exeptions on HTTP errors; + - raise exceptions on HTTP errors; - pluggable authentication; - store authentication information in a keyring; - store time spent for requests; diff --git a/cinderclient/openstack/common/apiclient/exceptions.py b/cinderclient/openstack/common/apiclient/exceptions.py index b03def7..4776d58 100644 --- a/cinderclient/openstack/common/apiclient/exceptions.py +++ b/cinderclient/openstack/common/apiclient/exceptions.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov @@ -22,8 +20,11 @@ Exception definitions. """ +import inspect import sys +import six + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -59,6 +60,11 @@ class AuthorizationFailure(ClientException): pass +class ConnectionRefused(ClientException): + """Cannot connect to API service.""" + pass + + class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): @@ -387,20 +393,12 @@ class HttpVersionNotSupported(HttpServerError): 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 +# _code_map contains all the classes that have http_status attribute. +_code_map = dict( + (getattr(obj, 'http_status', None), obj) + for name, obj in six.iteritems(vars(sys.modules[__name__])) + if inspect.isclass(obj) and getattr(obj, 'http_status', False) +) def from_response(response, method, url): diff --git a/cinderclient/openstack/common/apiclient/fake_client.py b/cinderclient/openstack/common/apiclient/fake_client.py index 914cebd..2d1c0aa 100644 --- a/cinderclient/openstack/common/apiclient/fake_client.py +++ b/cinderclient/openstack/common/apiclient/fake_client.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 OpenStack Foundation # All Rights Reserved. # @@ -27,11 +25,12 @@ places where actual behavior differs from the spec. # pylint: disable=W0102 import json -import urlparse import requests +import six from cinderclient.openstack.common.apiclient import client +from cinderclient.openstack.common.py3kcompat import urlutils def assert_has_keys(dct, required=[], optional=[]): @@ -63,6 +62,8 @@ class TestResponse(requests.Response): else: self._content = text default_headers = {} + if six.PY3 and isinstance(self._content, six.string_types): + self._content = self._content.encode('utf-8', 'strict') self.headers = data.get('headers') or default_headers else: self.status_code = data @@ -146,7 +147,7 @@ class FakeHTTPClient(client.HTTPClient): "text": fixture[1]}) # Call the method - args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) + args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') diff --git a/cinderclient/openstack/common/gettextutils.py b/cinderclient/openstack/common/gettextutils.py index ce57f89..240ac05 100644 --- a/cinderclient/openstack/common/gettextutils.py +++ b/cinderclient/openstack/common/gettextutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 Red Hat, Inc. # Copyright 2013 IBM Corp. # All Rights Reserved. @@ -26,13 +24,10 @@ Usual usage in an openstack.common module: import copy import gettext -import logging +import locale +from logging import handlers import os import re -try: - import UserString as _userString -except ImportError: - import collections as _userString from babel import localedata import six @@ -58,7 +53,7 @@ def enable_lazy(): def _(msg): if USE_LAZY: - return Message(msg, 'cinderclient') + return Message(msg, domain='cinderclient') else: if six.PY3: return _t.gettext(msg) @@ -90,11 +85,6 @@ def install(domain, lazy=False): # messages in OpenStack. We override the standard _() function # and % (format string) operation to build Message objects that can # later be translated when we have more information. - # - # Also included below is an example LocaleHandler that translates - # Messages to an associated locale, effectively allowing many logs, - # each with their own locale. - def _lazy_gettext(msg): """Create and return a Message object. @@ -105,7 +95,7 @@ def install(domain, lazy=False): Message encapsulates a string so that we can translate it later when needed. """ - return Message(msg, domain) + return Message(msg, domain=domain) from six import moves moves.builtins.__dict__['_'] = _lazy_gettext @@ -120,182 +110,169 @@ def install(domain, lazy=False): unicode=True) -class Message(_userString.UserString, object): - """Class used to encapsulate translatable messages.""" - def __init__(self, msg, domain): - # _msg is the gettext msgid and should never change - self._msg = msg - self._left_extra_msg = '' - self._right_extra_msg = '' - self._locale = None - self.params = None - self.domain = domain +class Message(six.text_type): + """A Message object is a unicode object that can be translated. - @property - def data(self): - # NOTE(mrodden): this should always resolve to a unicode string - # that best represents the state of the message currently + Translation of Message is done explicitly using the translate() method. + For all non-translation intents and purposes, a Message is simply unicode, + and can be treated as such. + """ - localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') - if self.locale: - lang = gettext.translation(self.domain, - localedir=localedir, - languages=[self.locale], - fallback=True) - else: - # use system locale for translations - lang = gettext.translation(self.domain, - localedir=localedir, - fallback=True) + def __new__(cls, msgid, msgtext=None, params=None, + domain='cinderclient', *args): + """Create a new Message object. + In order for translation to work gettext requires a message ID, this + msgid will be used as the base unicode text. It is also possible + for the msgid and the base unicode text to be different by passing + the msgtext parameter. + """ + # If the base msgtext is not given, we use the default translation + # of the msgid (which is in English) just in case the system locale is + # not English, so that the base text will be in that locale by default. + if not msgtext: + msgtext = Message._translate_msgid(msgid, domain) + # We want to initialize the parent unicode with the actual object that + # would have been plain unicode if 'Message' was not enabled. + msg = super(Message, cls).__new__(cls, msgtext) + msg.msgid = msgid + msg.domain = domain + msg.params = params + return msg + + def translate(self, desired_locale=None): + """Translate this message to the desired locale. + + :param desired_locale: The desired locale to translate the message to, + if no locale is provided the message will be + translated to the system's default locale. + + :returns: the translated message in unicode + """ + + translated_message = Message._translate_msgid(self.msgid, + self.domain, + desired_locale) + if self.params is None: + # No need for more translation + return translated_message + + # This Message object may have been formatted with one or more + # Message objects as substitution arguments, given either as a single + # argument, part of a tuple, or as one or more values in a dictionary. + # When translating this Message we need to translate those Messages too + translated_params = _translate_args(self.params, desired_locale) + + translated_message = translated_message % translated_params + + return translated_message + + @staticmethod + def _translate_msgid(msgid, domain, desired_locale=None): + if not desired_locale: + system_locale = locale.getdefaultlocale() + # If the system locale is not available to the runtime use English + if not system_locale[0]: + desired_locale = 'en_US' + else: + desired_locale = system_locale[0] + + locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) if six.PY3: - ugettext = lang.gettext + translator = lang.gettext else: - ugettext = lang.ugettext + translator = lang.ugettext - full_msg = (self._left_extra_msg + - ugettext(self._msg) + - self._right_extra_msg) + translated_message = translator(msgid) + return translated_message - if self.params is not None: - full_msg = full_msg % self.params + def __mod__(self, other): + # When we mod a Message we want the actual operation to be performed + # by the parent class (i.e. unicode()), the only thing we do here is + # save the original msgid and the parameters in case of a translation + params = self._sanitize_mod_params(other) + unicode_mod = super(Message, self).__mod__(params) + modded = Message(self.msgid, + msgtext=unicode_mod, + params=params, + domain=self.domain) + return modded - return six.text_type(full_msg) + def _sanitize_mod_params(self, other): + """Sanitize the object being modded with this Message. - @property - def locale(self): - return self._locale + - Add support for modding 'None' so translation supports it + - Trim the modded object, which can be a large dictionary, to only + those keys that would actually be used in a translation + - Snapshot the object being modded, in case the message is + translated, it will be used as it was when the Message was created + """ + if other is None: + params = (other,) + elif isinstance(other, dict): + params = self._trim_dictionary_parameters(other) + else: + params = self._copy_param(other) + return params - @locale.setter - def locale(self, value): - self._locale = value - if not self.params: - return + def _trim_dictionary_parameters(self, dict_param): + """Return a dict that only has matching entries in the msgid.""" + # NOTE(luisg): Here we trim down the dictionary passed as parameters + # to avoid carrying a lot of unnecessary weight around in the message + # object, for example if someone passes in Message() % locals() but + # only some params are used, and additionally we prevent errors for + # non-deepcopyable objects by unicoding() them. - # This Message object may have been constructed with one or more - # Message objects as substitution parameters, given as a single - # Message, or a tuple or Map containing some, so when setting the - # locale for this Message we need to set it for those Messages too. - if isinstance(self.params, Message): - self.params.locale = value - return - if isinstance(self.params, tuple): - for param in self.params: - if isinstance(param, Message): - param.locale = value - return - if isinstance(self.params, dict): - for param in self.params.values(): - if isinstance(param, Message): - param.locale = value + # Look for %(param) keys in msgid; + # Skip %% and deal with the case where % is first character on the line + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) - def _save_dictionary_parameter(self, dict_param): - full_msg = self.data - # look for %(blah) fields in string; - # ignore %% and deal with the - # case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) - - # if we don't find any %(blah) blocks but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): - # apparently the full dictionary is the parameter - params = copy.deepcopy(dict_param) + # If we don't find any %(param) keys but have a %s + if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): + # Apparently the full dictionary is the parameter + params = self._copy_param(dict_param) else: params = {} + # Save our existing parameters as defaults to protect + # ourselves from losing values if we are called through an + # (erroneous) chain that builds a valid Message with + # arguments, and then does something like "msg % kwds" + # where kwds is an empty dictionary. + src = {} + if isinstance(self.params, dict): + src.update(self.params) + src.update(dict_param) for key in keys: - try: - params[key] = copy.deepcopy(dict_param[key]) - except TypeError: - # cast uncopyable thing to unicode string - params[key] = six.text_type(dict_param[key]) + params[key] = self._copy_param(src[key]) return params - def _save_parameters(self, other): - # we check for None later to see if - # we actually have parameters to inject, - # so encapsulate if our parameter is actually None - if other is None: - self.params = (other, ) - elif isinstance(other, dict): - self.params = self._save_dictionary_parameter(other) - else: - # fallback to casting to unicode, - # this will handle the problematic python code-like - # objects that cannot be deep-copied - try: - self.params = copy.deepcopy(other) - except TypeError: - self.params = six.text_type(other) + def _copy_param(self, param): + try: + return copy.deepcopy(param) + except TypeError: + # Fallback to casting to unicode this will handle the + # python code-like objects that can't be deep-copied + return six.text_type(param) - return self - - # overrides to be more string-like - def __unicode__(self): - return self.data - - def __str__(self): - if six.PY3: - return self.__unicode__() - return self.data.encode('utf-8') - - def __getstate__(self): - to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', - 'domain', 'params', '_locale'] - new_dict = self.__dict__.fromkeys(to_copy) - for attr in to_copy: - new_dict[attr] = copy.deepcopy(self.__dict__[attr]) - - return new_dict - - def __setstate__(self, state): - for (k, v) in state.items(): - setattr(self, k, v) - - # operator overloads def __add__(self, other): - copied = copy.deepcopy(self) - copied._right_extra_msg += other.__str__() - return copied + msg = _('Message objects do not support addition.') + raise TypeError(msg) def __radd__(self, other): - copied = copy.deepcopy(self) - copied._left_extra_msg += other.__str__() - return copied + return self.__add__(other) - def __mod__(self, other): - # do a format string to catch and raise - # any possible KeyErrors from missing parameters - self.data % other - copied = copy.deepcopy(self) - return copied._save_parameters(other) - - def __mul__(self, other): - return self.data * other - - def __rmul__(self, other): - return other * self.data - - def __getitem__(self, key): - return self.data[key] - - def __getslice__(self, start, end): - return self.data.__getslice__(start, end) - - def __getattribute__(self, name): - # NOTE(mrodden): handle lossy operations that we can't deal with yet - # These override the UserString implementation, since UserString - # uses our __class__ attribute to try and build a new message - # after running the inner data string through the operation. - # At that point, we have lost the gettext message id and can just - # safely resolve to a string instead. - ops = ['capitalize', 'center', 'decode', 'encode', - 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', - 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] - if name in ops: - return getattr(self.data, name) - else: - return _userString.UserString.__getattribute__(self, name) + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) def get_available_languages(domain): @@ -317,49 +294,147 @@ def get_available_languages(domain): # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and all projects udpate + # this check when the master list updates to >=1.0, and update all projects list_identifiers = (getattr(localedata, 'list', None) or getattr(localedata, 'locale_identifiers')) locale_identifiers = list_identifiers() + for i in locale_identifiers: if find(i) is not None: language_list.append(i) + + # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported + # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they + # are perfectly legitimate locales: + # https://github.com/mitsuhiko/babel/issues/37 + # In Babel 1.3 they fixed the bug and they support these locales, but + # they are still not explicitly "listed" by locale_identifiers(). + # That is why we add the locales here explicitly if necessary so that + # they are listed as supported. + aliases = {'zh': 'zh_CN', + 'zh_Hant_HK': 'zh_HK', + 'zh_Hant': 'zh_TW', + 'fil': 'tl_PH'} + for (locale, alias) in six.iteritems(aliases): + if locale in language_list and alias not in language_list: + language_list.append(alias) + _AVAILABLE_LANGUAGES[domain] = language_list return copy.copy(language_list) -def get_localized_message(message, user_locale): - """Gets a localized version of the given message in the given locale.""" +def translate(obj, desired_locale=None): + """Gets the translated unicode representation of the given object. + + If the object is not translatable it is returned as-is. + If the locale is None the object is translated to the system locale. + + :param obj: the object to translate + :param desired_locale: the locale to translate the message to, if None the + default system locale will be used + :returns: the translated object in unicode, or the original object if + it could not be translated + """ + message = obj + if not isinstance(message, Message): + # If the object to translate is not already translatable, + # let's first get its unicode representation + message = six.text_type(obj) if isinstance(message, Message): - if user_locale: - message.locale = user_locale - return six.text_type(message) - else: - return message + # Even after unicoding() we still need to check if we are + # running with translatable unicode before translating + return message.translate(desired_locale) + return obj -class LocaleHandler(logging.Handler): - """Handler that can have a locale associated to translate Messages. +def _translate_args(args, desired_locale=None): + """Translates all the translatable elements of the given arguments object. - A quick example of how to utilize the Message class above. - LocaleHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating the internal Message. + This method is used for translating the translatable values in method + arguments which include values of tuples or dictionaries. + If the object is not a tuple or a dictionary the object itself is + translated if it is translatable. + + If the locale is None the object is translated to the system locale. + + :param args: the args to translate + :param desired_locale: the locale to translate the args to, if None the + default system locale will be used + :returns: a new args object with the translated contents of the original + """ + if isinstance(args, tuple): + return tuple(translate(v, desired_locale) for v in args) + if isinstance(args, dict): + translated_dict = {} + for (k, v) in six.iteritems(args): + translated_v = translate(v, desired_locale) + translated_dict[k] = translated_v + return translated_dict + return translate(args, desired_locale) + + +class TranslationHandler(handlers.MemoryHandler): + """Handler that translates records before logging them. + + The TranslationHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating them. This handler + depends on Message objects being logged, instead of regular strings. + + The handler can be configured declaratively in the logging.conf as follows: + + [handlers] + keys = translatedlog, translator + + [handler_translatedlog] + class = handlers.WatchedFileHandler + args = ('/var/log/api-localized.log',) + formatter = context + + [handler_translator] + class = openstack.common.log.TranslationHandler + target = translatedlog + args = ('zh_CN',) + + If the specified locale is not available in the system, the handler will + log in the default locale. """ - def __init__(self, locale, target): - """Initialize a LocaleHandler + def __init__(self, locale=None, target=None): + """Initialize a TranslationHandler :param locale: locale to use for translating messages :param target: logging.Handler object to forward LogRecord objects to after translation """ - logging.Handler.__init__(self) + # NOTE(luisg): In order to allow this handler to be a wrapper for + # other handlers, such as a FileHandler, and still be able to + # configure it using logging.conf, this handler has to extend + # MemoryHandler because only the MemoryHandlers' logging.conf + # parsing is implemented such that it accepts a target handler. + handlers.MemoryHandler.__init__(self, capacity=0, target=target) self.locale = locale - self.target = target + + def setFormatter(self, fmt): + self.target.setFormatter(fmt) def emit(self, record): - if isinstance(record.msg, Message): - # set the locale and resolve to a string - record.msg.locale = self.locale + # We save the message from the original record to restore it + # after translation, so other handlers are not affected by this + original_msg = record.msg + original_args = record.args + + try: + self._translate_and_log_record(record) + finally: + record.msg = original_msg + record.args = original_args + + def _translate_and_log_record(self, record): + record.msg = translate(record.msg, self.locale) + + # In addition to translating the message, we also need to translate + # arguments that were passed to the log method that were not part + # of the main message e.g., log.info(_('Some message %s'), this_one)) + record.args = _translate_args(record.args, self.locale) self.target.emit(record) diff --git a/cinderclient/openstack/common/importutils.py b/cinderclient/openstack/common/importutils.py index 7a303f9..4fd9ae2 100644 --- a/cinderclient/openstack/common/importutils.py +++ b/cinderclient/openstack/common/importutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2011 OpenStack Foundation. # All Rights Reserved. # diff --git a/cinderclient/openstack/common/py3kcompat/__init__.py b/cinderclient/openstack/common/py3kcompat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cinderclient/openstack/common/py3kcompat/urlutils.py b/cinderclient/openstack/common/py3kcompat/urlutils.py new file mode 100644 index 0000000..84e457a --- /dev/null +++ b/cinderclient/openstack/common/py3kcompat/urlutils.py @@ -0,0 +1,67 @@ +# +# Copyright 2013 Canonical Ltd. +# 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. +# + +""" +Python2/Python3 compatibility layer for OpenStack +""" + +import six + +if six.PY3: + # python3 + import urllib.error + import urllib.parse + import urllib.request + + urlencode = urllib.parse.urlencode + urljoin = urllib.parse.urljoin + quote = urllib.parse.quote + quote_plus = urllib.parse.quote_plus + parse_qsl = urllib.parse.parse_qsl + unquote = urllib.parse.unquote + unquote_plus = urllib.parse.unquote_plus + urlparse = urllib.parse.urlparse + urlsplit = urllib.parse.urlsplit + urlunsplit = urllib.parse.urlunsplit + SplitResult = urllib.parse.SplitResult + + urlopen = urllib.request.urlopen + URLError = urllib.error.URLError + pathname2url = urllib.request.pathname2url +else: + # python2 + import urllib + import urllib2 + import urlparse + + urlencode = urllib.urlencode + quote = urllib.quote + quote_plus = urllib.quote_plus + unquote = urllib.unquote + unquote_plus = urllib.unquote_plus + + parse = urlparse + parse_qsl = parse.parse_qsl + urljoin = parse.urljoin + urlparse = parse.urlparse + urlsplit = parse.urlsplit + urlunsplit = parse.urlunsplit + SplitResult = parse.SplitResult + + urlopen = urllib2.urlopen + URLError = urllib2.URLError + pathname2url = urllib.pathname2url diff --git a/cinderclient/openstack/common/strutils.py b/cinderclient/openstack/common/strutils.py index 7c9fcec..ee71db0 100644 --- a/cinderclient/openstack/common/strutils.py +++ b/cinderclient/openstack/common/strutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2011 OpenStack Foundation. # All Rights Reserved. # @@ -25,7 +23,7 @@ import unicodedata import six -from cinderclient.openstack.common.gettextutils import _ # noqa +from cinderclient.openstack.common.gettextutils import _ # Used for looking up extensions of text @@ -60,12 +58,12 @@ def int_from_bool_as_string(subject): return bool_from_string(subject) and 1 or 0 -def bool_from_string(subject, strict=False): +def bool_from_string(subject, strict=False, default=False): """Interpret a string as a boolean. A case-insensitive match is performed such that strings matching 't', 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else is considered False. + `strict=False`, anything else returns the value specified by 'default'. Useful for JSON-decoded stuff and config file parsing. @@ -90,7 +88,7 @@ def bool_from_string(subject, strict=False): 'acceptable': acceptable} raise ValueError(msg) else: - return False + return default def safe_decode(text, incoming=None, errors='strict'): @@ -101,7 +99,7 @@ def safe_decode(text, incoming=None, errors='strict'): values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) @@ -144,7 +142,7 @@ def safe_encode(text, incoming=None, values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) @@ -154,11 +152,17 @@ def safe_encode(text, incoming=None, sys.getdefaultencoding()) if isinstance(text, six.text_type): - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) return text diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 92d66ae..46822e3 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 OpenStack Foundation # Copyright 2013 IBM Corp. # @@ -121,9 +119,6 @@ class InstallVenv(object): self.pip_install('-r', self.requirements, '-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() @@ -156,14 +151,6 @@ class Distro(InstallVenv): ' 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. @@ -175,10 +162,6 @@ class Fedora(Distro): 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 @@ -187,27 +170,3 @@ class Fedora(Distro): 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 - """ - - if os.path.exists('contrib/redhat-eventlet.patch'): - # 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 306d451bc7fc6c33c4aa222582be76c43d835866 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Mon, 2 Dec 2013 18:56:45 -0500 Subject: [PATCH 158/165] Add shell tests for snapshot_delete snapshot_delete does not have a shell test covering it. Closes-Bug: #1257747 Change-Id: I22252f5025c11a7c115aa75e07c78424eec75ba1 --- cinderclient/tests/v1/fakes.py | 3 +++ cinderclient/tests/v1/test_shell.py | 4 ++++ cinderclient/tests/v2/fakes.py | 3 +++ cinderclient/tests/v2/test_shell.py | 4 ++++ doc/source/index.rst | 1 + 5 files changed, 15 insertions(+) diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 6fb7f54..648733c 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -282,6 +282,9 @@ class FakeHTTPClient(base_client.HTTPClient): def post_snapshots_5678_action(self, body, **kw): return self.post_snapshots_1234_action(body, **kw) + def delete_snapshots_1234(self, **kw): + return (202, {}, {}) + # # Volumes # diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index 50293cf..8c2e33d 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -351,3 +351,7 @@ class ShellTest(utils.TestCase): self.run_command('service-enable host cinder-volume') self.assert_called('PUT', '/os-services/enable', {"binary": "cinder-volume", "host": "host"}) + + def test_snapshot_delete(self): + self.run_command('snapshot-delete 1234') + self.assert_called('DELETE', '/snapshots/1234') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 086034a..890fa53 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -291,6 +291,9 @@ class FakeHTTPClient(base_client.HTTPClient): def post_snapshots_5678_action(self, body, **kw): return self.post_snapshots_1234_action(body, **kw) + def delete_snapshots_1234(self, **kw): + return (202, {}, {}) + # # Volumes # diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index 85a230a..4946ada 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -341,3 +341,7 @@ class ShellTest(utils.TestCase): expected = {'os-retype': {'new_type': 'foo', 'migration_policy': 'never'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + + def test_snapshot_delete(self): + self.run_command('snapshot-delete 1234') + self.assert_called('DELETE', '/snapshots/1234') diff --git a/doc/source/index.rst b/doc/source/index.rst index 6c4df22..f7a6f93 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -46,6 +46,7 @@ MASTER .. _1264415: http://bugs.launchpad.net/python-cinderclient/+bug/1264415 .. _1258489: http://bugs.launchpad.net/python-cinderclient/+bug/1258489 .. _1248519: http://bugs.launchpad.net/python-cinderclient/+bug/1248519 +.. _1257747: http://bugs.launchpad.net/python-cinderclient/+bug/1257747 1.0.7 ----- From 019f43f63fe363572601fc8984ae2f13ce2b6fa6 Mon Sep 17 00:00:00 2001 From: Andrew Kerr Date: Thu, 30 Jan 2014 11:07:07 -0500 Subject: [PATCH 159/165] Fixed image_name from image-name in upload-to-image This fix addresses an issue where the variable name for image_name was incorrect, causing the image_name parameter to be sent as null when using "cinder upload-to-image" from cli with v2 of the API Change-Id: I62f8bbb4397526fb291a46777d9025318da8e5b3 Closes-Bug: 1274552 --- 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 5c1e86f..10246f8 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -869,7 +869,7 @@ def _find_volume_type(cs, vtype): default='raw') @utils.arg('--disk_format', help=argparse.SUPPRESS) -@utils.arg('image-name', +@utils.arg('image_name', metavar='', help='Name for created image') @utils.arg('--image_name', From 305f7609397d1b10d161794a309ecb5dd49d545d Mon Sep 17 00:00:00 2001 From: john-griffith Date: Fri, 31 Jan 2014 08:33:15 -0700 Subject: [PATCH 160/165] Update release notes for push to pypi Need to get a 1.08 out, so just modifying the release notes for the push version. Change-Id: Icf9684ab0fabf0168662c3ff4047db54a2670d11 --- doc/source/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index f7a6f93..8eab249 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,8 +29,9 @@ See also :doc:`/man/cinder`. Release Notes ============= -MASTER ------- + +1.0.8 +----- * Add support for reset-state on multiple volumes or snapshots at once * Add volume retype command From e31e057cccd4e79bcb989ddbfd1a47c5d801d51a Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Fri, 31 Jan 2014 13:55:09 -0800 Subject: [PATCH 161/165] Revert "Update cinderclient to skip the additional GET during create" This reverts commit 47014971b09f0334e5cc3b414bef31784f88a37d. Conflicts: doc/source/index.rst Change-Id: Ica1e526b2c181d1ce72183b7293d21119371c822 --- cinderclient/v2/shell.py | 1 + doc/source/index.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 10246f8..636c812 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -296,6 +296,7 @@ def do_create(cs, args): scheduler_hints=hints) info = dict() + volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links', None) diff --git a/doc/source/index.rst b/doc/source/index.rst index 8eab249..8341aa7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -36,7 +36,6 @@ Release Notes * Add volume retype command .. _966329: https://bugs.launchpad.net/python-cinderclient/+bug/966329 -.. _1265893: https://bugs.launchpad.net/python-cinderclient/+bug/1265893 .. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 From 0fc03977b6eb89c1e94d51aee5e515cd7a3b3def Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 9 Apr 2014 00:49:33 +0800 Subject: [PATCH 162/165] Standards-Version: 3.9.5 --- debian/changelog | 7 +++++++ debian/control | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index e247765..a95a377 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +python-cinderclient (1:1.0.8-1) experimental; urgency=low + + * New upstream release. + * Standards-Version: is now 3.9.5. + + -- Thomas Goirand Wed, 09 Apr 2014 00:46:18 +0800 + python-cinderclient (1:1.0.6-2) unstable; urgency=low * Added missing build-depends: python-babel. diff --git a/debian/control b/debian/control index 0ae6b50..efe653d 100644 --- a/debian/control +++ b/debian/control @@ -26,7 +26,7 @@ Build-Depends-Indep: python-babel, python-testtools (>= 0.9.32), python-unittest2, testrepository (>= 0.0.17) -Standards-Version: 3.9.4 +Standards-Version: 3.9.5 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary Vcs-Git: git://anonscm.debian.org/openstack/python-cinderclient.git Homepage: https://github.com/openstack/python-cinderclient From 7f0e8eb12b8518f753ac09c30da3ce6636c6868c Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 9 Apr 2014 00:53:45 +0800 Subject: [PATCH 163/165] New (build-)depends for this release. --- debian/changelog | 1 + debian/control | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/debian/changelog b/debian/changelog index a95a377..0cf7631 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ python-cinderclient (1:1.0.8-1) experimental; urgency=low * New upstream release. * Standards-Version: is now 3.9.5. + * New (build-)depends for this release. -- Thomas Goirand Wed, 09 Apr 2014 00:46:18 +0800 diff --git a/debian/control b/debian/control index efe653d..4eee077 100644 --- a/debian/control +++ b/debian/control @@ -11,20 +11,21 @@ Build-Depends: debhelper (>= 9), python-all (>= 2.6.6-3~), python-pbr (>= 0.5.21), python-setuptools -Build-Depends-Indep: python-babel, - python-fixtures (>= 0.3.12), +Build-Depends-Indep: python-babel (>= 1.3), + python-fixtures (>= 0.3.14), + python-hacking (>= 0.8.0), python-httplib2, - python-mock, + python-mock (>= 1.0), python-nose, - python-prettytable, + python-prettytable (>= 0.7), python-requests (>= 1.1), - python-setuptools-git, python-simplejson (>= 2.0.9), - python-six, + python-six (>= 1.4.1), python-sphinx, python-subunit, python-testtools (>= 0.9.32), python-unittest2, + subunit, testrepository (>= 0.0.17) Standards-Version: 3.9.5 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-cinderclient.git;a=summary @@ -34,12 +35,13 @@ Homepage: https://github.com/openstack/python-cinderclient Package: python-cinderclient Architecture: all Pre-Depends: dpkg (>= 1.15.6~) -Depends: python-httplib2, +Depends: python-babel (>= 1.3), + python-httplib2, python-pbr (>= 0.5.21), - python-prettytable, + python-prettytable (>= 0.7), python-requests (>= 1.1), python-simplejson (>= 2.0.9), - python-six, + python-six (>= 1.4.1), ${misc:Depends}, ${python:Depends} Provides: ${python:Provides} From 7e8cefda2c0864ffa9651429a4dcb22bb2fc2096 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 9 Apr 2014 00:56:59 +0800 Subject: [PATCH 164/165] Disabled useless dh targets. --- debian/changelog | 1 + debian/rules | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0cf7631..b69821b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,6 +3,7 @@ python-cinderclient (1:1.0.8-1) experimental; urgency=low * New upstream release. * Standards-Version: is now 3.9.5. * New (build-)depends for this release. + * Disabled useless dh targets. -- Thomas Goirand Wed, 09 Apr 2014 00:46:18 +0800 diff --git a/debian/rules b/debian/rules index 30bd43a..d930d47 100755 --- a/debian/rules +++ b/debian/rules @@ -30,3 +30,11 @@ override_dh_auto_test: ./run_tests.sh -P -N || true #python setup.py testr --slowest --testr-args='--subunit ' endif + +# Commands not to run +override_dh_installcatalogs: +override_dh_installemacsen override_dh_installifupdown: +override_dh_installinfo override_dh_installmenu override_dh_installmime: +override_dh_installmodules override_dh_installlogcheck: +override_dh_installpam override_dh_installppp override_dh_installudev override_dh_installwm: +override_dh_installxfonts override_dh_gconf override_dh_icons override_dh_perl override_dh_usrlocal: From 2ed244dca9e427751020c839d95af474c7a80020 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Fri, 18 Apr 2014 06:37:10 +0000 Subject: [PATCH 165/165] Uploading to unstable. --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index b69821b..555b631 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-cinderclient (1:1.0.8-2) unstable; urgency=medium + + * Uploading to unstable. + + -- Thomas Goirand Fri, 18 Apr 2014 06:36:30 +0000 + python-cinderclient (1:1.0.8-1) experimental; urgency=low * New upstream release.