Merge tag '1.2.0' into debian/liberty

python-cinderclient 1.2.0 release
This commit is contained in:
Corey Bryant
2015-06-08 11:05:40 -04:00
89 changed files with 2780 additions and 812 deletions

View File

@@ -1,7 +1,7 @@
[run]
branch = True
source = cinderclient
omit = cinderclient/openstack/*
omit = cinderclient/openstack/*,cinderclient/tests/*
[report]
ignore-errors = True

13
.gitignore vendored
View File

@@ -1,7 +1,8 @@
.coverage
.venv
.tox
.testrepository
/.*
!.gitignore
!.mailmap
!.testr.conf
.*.sw?
subunit.log
*,cover
cover
@@ -13,7 +14,3 @@ build
dist
cinderclient/versioninfo
python_cinderclient.egg-info
# Development environment files
.project
.pydevproject

View File

@@ -1,4 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./cinderclient/tests $LISTOPT $IDOPTION
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./cinderclient/tests} $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

12
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,12 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in the "If you're a developer"
section of this page: [http://wiki.openstack.org/HowToContribute](http://wiki.openstack.org/HowToContribute#If_you.27re_a_developer:)
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at [http://wiki.openstack.org/GerritWorkflow](http://wiki.openstack.org/GerritWorkflow).
Pull requests submitted through GitHub will be ignored.
Bugs should be filed [on Launchpad](https://bugs.launchpad.net/python-cinderclient),
not in GitHub's issue tracker.

View File

@@ -18,7 +18,7 @@ pull requests.
.. _Github: https://github.com/openstack/python-cinderclient
.. _Launchpad: https://launchpad.net/python-cinderclient
.. _Gerrit: http://wiki.openstack.org/GerritWorkflow
.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
This code a fork of `Jacobian's python-cloudservers`__ If you need API support
for the Rackspace API solely or the BSD license, you should use that repository.
@@ -110,7 +110,7 @@ You'll find complete documentation on the shell by running
list-extensions List all the os-api extensions that are available.
Optional arguments:
--debug Print debugging output
-d, --debug Print debugging output
--os-username <auth-user-name>
Defaults to env[OS_USERNAME].
--os-password <auth-password>

View File

@@ -33,14 +33,6 @@ from cinderclient import utils
Resource = common_base.Resource
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
def getid(obj):
"""
Abstracts the common pattern of allowing both an object or an object's ID
@@ -62,8 +54,11 @@ class Manager(utils.HookableMixin):
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
def _list(self, url, response_key, obj_class=None, body=None,
limit=None, items=None):
resp = None
if items is None:
items = []
if body:
resp, body = self.api.client.post(url, body=body)
else:
@@ -83,8 +78,37 @@ class Manager(utils.HookableMixin):
with self.completion_cache('human_id', obj_class, mode="w"):
with self.completion_cache('uuid', obj_class, mode="w"):
return [obj_class(self, res, loaded=True)
for res in data if res]
items_new = [obj_class(self, res, loaded=True)
for res in data if res]
if limit:
limit = int(limit)
margin = limit - len(items)
if margin <= len(items_new):
# If the limit is reached, return the items.
items = items + items_new[:margin]
return items
else:
items = items + items_new
else:
items = items + items_new
# It is possible that the length of the list we request is longer
# than osapi_max_limit, so we have to retrieve multiple times to
# get the complete list.
next = None
if 'volumes_links' in body:
volumes_links = body['volumes_links']
if volumes_links:
for volumes_link in volumes_links:
if 'rel' in volumes_link and 'next' == volumes_link['rel']:
next = volumes_link['href']
break
if next:
# As long as the 'next' link is not empty, keep requesting it
# till there is no more items.
items = self._list(next, response_key, obj_class, None,
limit, items)
return items
@contextlib.contextmanager
def completion_cache(self, cache_type, obj_class, mode):
@@ -165,9 +189,11 @@ class Manager(utils.HookableMixin):
def _delete(self, url):
resp, body = self.api.client.delete(url)
def _update(self, url, body, **kwargs):
def _update(self, url, body, response_key=None, **kwargs):
self.run_hooks('modify_body_for_update', body, **kwargs)
resp, body = self.api.client.put(url, body=body)
if response_key:
return self.resource_class(self, body[response_key], loaded=True)
return body

View File

@@ -28,9 +28,11 @@ from keystoneclient.auth.identity import base
import requests
from cinderclient import exceptions
from cinderclient.openstack.common.gettextutils import _
from cinderclient.openstack.common import importutils
from cinderclient.openstack.common import strutils
from cinderclient import utils
osprofiler_web = importutils.try_import("osprofiler.web")
try:
import urlparse
@@ -64,20 +66,37 @@ def get_volume_api_from_url(url):
return version[1:]
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(valid_versions)))
(version, ', '.join(_VALID_VERSIONS)))
raise exceptions.UnsupportedVersion(msg)
class SessionClient(adapter.LegacyJsonAdapter):
def __init__(self, **kwargs):
kwargs.setdefault('user_agent', 'python-cinderclient')
kwargs.setdefault('service_type', 'volume')
super(SessionClient, self).__init__(**kwargs)
def request(self, *args, **kwargs):
def request(self, url, method, **kwargs):
kwargs.setdefault('authenticated', False)
return super(SessionClient, self).request(*args, **kwargs)
# NOTE(thingee): v1 and v2 require the project id in the url. Prepend
# it if we're doing discovery. We figure out if we're doing discovery
# if there is no project id already specified in the path. parts is
# a list where index 1 is the version discovered and index 2 might be
# an empty string or a project id.
endpoint = self.get_endpoint()
parts = urlparse.urlsplit(endpoint).path.split('/')
project_id = self.get_project_id()
if (parts[1] in ['v1', 'v2'] and parts[2] == ''
and project_id is not None):
url = '{0}{1}{2}'.format(endpoint, project_id, url)
# Note(tpatil): The standard call raises errors from
# keystoneclient, here we need to raise the cinderclient errors.
raise_exc = kwargs.pop('raise_exc', True)
resp, body = super(SessionClient, self).request(url, method,
raise_exc=False,
**kwargs)
if raise_exc and resp.status_code >= 400:
raise exceptions.from_response(resp, body)
return resp, body
def _cs_request(self, url, method, **kwargs):
# this function is mostly redundant but makes compatibility easier
@@ -96,32 +115,19 @@ class SessionClient(adapter.LegacyJsonAdapter):
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
def _invalidate(self, auth=None):
# NOTE(jamielennox): This is being implemented in keystoneclient
return self.session.invalidate(auth or self.auth)
def _get_token(self, auth=None):
# NOTE(jamielennox): This is being implemented in keystoneclient
return self.session.get_token(auth or self.auth)
def _get_endpoint(self, auth=None, **kwargs):
# NOTE(jamielennox): This is being implemented in keystoneclient
if self.service_type:
kwargs.setdefault('service_type', self.service_type)
if self.service_name:
kwargs.setdefault('service_name', self.service_name)
if self.interface:
kwargs.setdefault('interface', self.interface)
if self.region_name:
kwargs.setdefault('region_name', self.region_name)
return self.session.get_endpoint(auth or self.auth, **kwargs)
def get_volume_api_version_from_endpoint(self):
return get_volume_api_from_url(self._get_endpoint())
endpoint = self.get_endpoint()
if not endpoint:
msg = _('The Cinder server does not support %s. Check your '
'providers supported versions and try again with '
'setting --os-volume-api-version or the environment '
'variable OS_VOLUME_API_VERSION.') % self.version
raise exceptions.InvalidAPIVersion(msg)
return get_volume_api_from_url(endpoint)
def authenticate(self, auth=None):
self._invalidate(auth)
return self._get_token(auth)
self.invalidate(auth)
return self.get_token(auth)
@property
def service_catalog(self):
@@ -143,7 +149,8 @@ class HTTPClient(object):
insecure=False, timeout=None, tenant_id=None,
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', service_type=None,
service_name=None, volume_service_name=None, retries=None,
service_name=None, volume_service_name=None,
bypass_url=None, retries=None,
http_log_debug=False, cacert=None,
auth_system='keystone', auth_plugin=None):
self.user = user
@@ -159,17 +166,18 @@ class HTTPClient(object):
if not auth_url:
raise exceptions.EndpointNotFound()
self.auth_url = auth_url.rstrip('/')
self.auth_url = auth_url.rstrip('/') if auth_url else None
self.version = 'v1'
self.region_name = region_name
self.endpoint_type = endpoint_type
self.service_type = service_type
self.service_name = service_name
self.volume_service_name = volume_service_name
self.bypass_url = bypass_url.rstrip('/') if bypass_url else bypass_url
self.retries = int(retries or 0)
self.http_log_debug = http_log_debug
self.management_url = None
self.management_url = self.bypass_url or None
self.auth_token = None
self.proxy_token = proxy_token
self.proxy_tenant_id = proxy_tenant_id
@@ -224,6 +232,10 @@ class HTTPClient(object):
kwargs.setdefault('headers', kwargs.get('headers', {}))
kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json'
if osprofiler_web:
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['body'])
@@ -292,6 +304,10 @@ class HTTPClient(object):
if attempts > self.retries:
msg = 'Unable to establish connection: %s' % e
raise exceptions.ConnectionError(msg)
except requests.exceptions.Timeout as e:
self._logger.debug("Timeout error: %s" % e)
if attempts > self.retries:
raise
self._logger.debug(
"Failed attempt(%s of %s), retrying in %s seconds" %
(attempts, self.retries, backoff))
@@ -400,7 +416,10 @@ class HTTPClient(object):
# existing token? If so, our actual endpoints may
# be different than that of the admin token.
if self.proxy_token:
self._fetch_endpoints_from_auth(admin_url)
if self.bypass_url:
self.set_management_url(self.bypass_url)
else:
self._fetch_endpoints_from_auth(admin_url)
# Since keystone no longer returns the user token
# with the endpoints any more, we need to replace
# our service account token with the user token.
@@ -417,6 +436,11 @@ class HTTPClient(object):
auth_url = auth_url + '/v2.0'
self._v2_auth(auth_url)
if self.bypass_url:
self.set_management_url(self.bypass_url)
elif not self.management_url:
raise exceptions.Unauthorized('Cinder Client')
def _v1_auth(self, url):
if self.proxy_token:
raise exceptions.NoTokenLookupException()
@@ -476,7 +500,7 @@ def _construct_http_client(username=None, password=None, project_id=None,
region_name=None, endpoint_type='publicURL',
service_type='volume',
service_name=None, volume_service_name=None,
retries=None,
bypass_url=None, retries=None,
http_log_debug=False,
auth_system='keystone', auth_plugin=None,
cacert=None, tenant_id=None,
@@ -484,7 +508,9 @@ def _construct_http_client(username=None, password=None, project_id=None,
auth=None,
**kwargs):
if session:
# Don't use sessions if third party plugin is used
if session and not auth_plugin:
kwargs.setdefault('user_agent', 'python-cinderclient')
kwargs.setdefault('interface', endpoint_type)
return SessionClient(session=session,
auth=auth,
@@ -509,6 +535,7 @@ def _construct_http_client(username=None, password=None, project_id=None,
service_type=service_type,
service_name=service_name,
volume_service_name=volume_service_name,
bypass_url=bypass_url,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert,
@@ -529,9 +556,9 @@ def get_client_class(version):
(version, ', '.join(version_map)))
raise exceptions.UnsupportedVersion(msg)
return utils.import_class(client_path)
return importutils.import_class(client_path)
def Client(version, *args, **kwargs):
client_class = get_client_class(version)
return client_class(*args, **kwargs)
return client_class(*args, version=version, **kwargs)

View File

@@ -42,7 +42,7 @@ class NoUniqueMatch(Exception):
class AuthSystemNotFound(Exception):
"""When the user specify a AuthSystem but not installed."""
"""When the user specifies an AuthSystem but not installed."""
def __init__(self, auth_system):
self.auth_system = auth_system

View File

@@ -21,6 +21,7 @@ Command-line interface to the OpenStack Cinder API.
from __future__ import print_function
import argparse
import getpass
import glob
import imp
import itertools
@@ -36,18 +37,21 @@ from cinderclient import exceptions as exc
from cinderclient import utils
import cinderclient.auth_plugin
import cinderclient.extension
from cinderclient.openstack.common import importutils
from cinderclient.openstack.common import strutils
from cinderclient.openstack.common.gettextutils import _
from cinderclient.v1 import shell as shell_v1
from cinderclient.v2 import shell as shell_v2
from keystoneclient import adapter
from keystoneclient import discover
from keystoneclient import session
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient.exceptions import DiscoveryFailure
from keystoneclient import exceptions as keystoneclient_exc
import six.moves.urllib.parse as urlparse
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
DEFAULT_OS_VOLUME_API_VERSION = "1"
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
@@ -122,7 +126,7 @@ class OpenStackCinderShell(object):
action='version',
version=cinderclient.__version__)
parser.add_argument('--debug',
parser.add_argument('-d', '--debug',
action='store_true',
default=utils.env('CINDERCLIENT_DEBUG',
default=False),
@@ -180,12 +184,33 @@ class OpenStackCinderShell(object):
parser.add_argument('--os_volume_api_version',
help=argparse.SUPPRESS)
parser.add_argument('--bypass-url',
metavar='<bypass-url>',
dest='bypass_url',
default=utils.env('CINDERCLIENT_BYPASS_URL'),
help="Use this API endpoint instead of the "
"Service Catalog. Defaults to "
"env[CINDERCLIENT_BYPASS_URL]")
parser.add_argument('--bypass_url',
help=argparse.SUPPRESS)
parser.add_argument('--retries',
metavar='<retries>',
type=int,
default=0,
help='Number of retries.')
if osprofiler_profiler:
parser.add_argument('--profile',
metavar='HMAC_KEY',
help='HMAC key to use for encrypting context '
'data for performance profiling of operation. '
'This key needs to match the one configured '
'on the cinder api server. '
'Without key the profiling will not be '
'triggered even if osprofiler is enabled '
'on server side.')
self._append_global_identity_args(parser)
# The auth-system-plugins might require some extra options
@@ -490,13 +515,35 @@ class OpenStackCinderShell(object):
ks_logger = logging.getLogger("keystoneclient")
ks_logger.setLevel(logging.DEBUG)
def main(self, argv):
def _delimit_metadata_args(self, argv):
"""This function adds -- separator at the appropriate spot
"""
word = '--metadata'
tmp = []
# flag is true in between metadata option and next option
metadata_options = False
if word in argv:
for arg in argv:
if arg == word:
metadata_options = True
elif metadata_options:
if arg.startswith('--'):
metadata_options = False
elif '=' not in arg:
tmp.append(u'--')
metadata_options = False
tmp.append(arg)
return tmp
else:
return argv
def main(self, argv):
# Parse args once to find version and debug settings
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)
api_version_input = True
service_type_input = True
self.options = options
if not options.os_volume_api_version:
@@ -506,6 +553,8 @@ class OpenStackCinderShell(object):
options.os_volume_api_version = DEFAULT_OS_VOLUME_API_VERSION
api_version_input = False
version = (options.os_volume_api_version,)
# build available subcommands based on version
self.extensions = self._discover_extensions(
options.os_volume_api_version)
@@ -519,6 +568,7 @@ class OpenStackCinderShell(object):
subcommand_parser.print_help()
return 0
argv = self._delimit_metadata_args(argv)
args = subcommand_parser.parse_args(argv)
self._run_extension_hooks('__post_parse_args__', args)
@@ -532,16 +582,16 @@ class OpenStackCinderShell(object):
(os_username, os_password, os_tenant_name, os_auth_url,
os_region_name, os_tenant_id, endpoint_type, insecure,
service_type, service_name, volume_service_name,
service_type, service_name, volume_service_name, bypass_url,
cacert, os_auth_system) = (
args.os_username, args.os_password,
args.os_tenant_name, args.os_auth_url,
args.os_region_name, args.os_tenant_id,
args.endpoint_type, args.insecure,
args.service_type, args.service_name,
args.volume_service_name, args.os_cacert,
args.volume_service_name,
args.bypass_url, args.os_cacert,
args.os_auth_system)
if os_auth_system and os_auth_system != "keystone":
auth_plugin = cinderclient.auth_plugin.load_plugin(os_auth_system)
else:
@@ -553,6 +603,7 @@ class OpenStackCinderShell(object):
if not service_type:
service_type = DEFAULT_CINDER_SERVICE_TYPE
service_type = utils.get_service_type(args.func) or service_type
service_type_input = False
# FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
@@ -568,9 +619,20 @@ class OpenStackCinderShell(object):
"env[OS_USERNAME].")
if not os_password:
raise exc.CommandError("You must provide a password "
"through --os-password or "
"env[OS_PASSWORD].")
# No password, If we've got a tty, try prompting for it
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
# Check for Ctl-D
try:
os_password = getpass.getpass('OS Password: ')
except EOFError:
pass
# No password because we didn't have a tty or the
# user Ctl-D when prompted.
if not os_password:
raise exc.CommandError("You must provide a password "
"through --os-password, "
"env[OS_PASSWORD] "
"or, prompted response.")
if not (os_tenant_name or os_tenant_id):
raise exc.CommandError("You must provide a tenant ID "
@@ -619,21 +681,75 @@ class OpenStackCinderShell(object):
"through --os-auth-url or env[OS_AUTH_URL].")
auth_session = self._get_keystone_session()
if not service_type_input or not api_version_input:
# NOTE(thingee): Unfortunately the v2 shell is tied to volumev2
# service_type. If the service_catalog just contains service_type
# volume with x.x.x.x:8776 for discovery, and the user sets version
# 2 for the client, it'll default to volumev2 and raise
# EndpointNotFound. This is a workaround until we feel comfortable
# with removing the volumev2 assumption.
keystone_adapter = adapter.Adapter(auth_session)
try:
# Try just the client's defaults
endpoint = keystone_adapter.get_endpoint(
service_type=service_type,
version=version,
interface='public')
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,
# Service was found, but wrong version. Lets try a different
# version, if the user did not specify one.
if not endpoint and not api_version_input:
if version == ('1',):
version = ('2',)
else:
version = ('1',)
endpoint = keystone_adapter.get_endpoint(
service_type=service_type, version=version,
interface='public')
except keystoneclient_exc.EndpointNotFound as e:
# No endpoint found with that service_type, lets fall back to
# other service_types if the user did not specify one.
if not service_type_input:
if service_type == 'volume':
service_type = 'volumev2'
else:
service_type = 'volume'
try:
endpoint = keystone_adapter.get_endpoint(
version=version,
service_type=service_type, interface='public')
# Service was found, but wrong version. Lets try
# a different version, if the user did not specify one.
if not endpoint and not api_version_input:
if version == ('1',):
version = ('2',)
else:
version = ('1',)
endpoint = keystone_adapter.get_endpoint(
service_type=service_type, version=version,
interface='public')
except keystoneclient_exc.EndpointNotFound:
raise e
self.cs = client.Client(version[0], os_username, os_password,
os_tenant_name, os_auth_url,
region_name=os_region_name,
tenant_id=os_tenant_id,
endpoint_type=endpoint_type,
extensions=self.extensions,
service_type=service_type,
service_name=service_name,
volume_service_name=volume_service_name,
retries=options.retries,
http_log_debug=args.debug,
cacert=cacert, auth_system=os_auth_system,
auth_plugin=auth_plugin,
session=auth_session)
bypass_url=bypass_url, retries=options.retries,
http_log_debug=args.debug, cacert=cacert,
auth_system=os_auth_system,
auth_plugin=auth_plugin, session=auth_session)
try:
if not utils.isunauthenticated(args.func):
@@ -651,7 +767,8 @@ class OpenStackCinderShell(object):
try:
endpoint_api_version = \
self.cs.get_volume_api_version_from_endpoint()
if endpoint_api_version != options.os_volume_api_version:
if (endpoint_api_version != options.os_volume_api_version
and api_version_input):
msg = (("OpenStack Block Storage API version is set to %s "
"but you are accessing a %s endpoint. "
"Change its value through --os-volume-api-version "
@@ -671,8 +788,18 @@ class OpenStackCinderShell(object):
"to the default API version: %s" %
endpoint_api_version)
profile = osprofiler_profiler and options.profile
if profile:
osprofiler_profiler.init(options.profile)
args.func(self.cs, args)
if profile:
trace_id = osprofiler_profiler.get().get_base_id()
print("Trace ID: %s" % trace_id)
print("To display trace use next command:\n"
"osprofiler trace show --html %s " % trace_id)
def _run_extension_hooks(self, hook_type, *args, **kwargs):
"""Runs hooks for all registered extensions."""
for extension in self.extensions:
@@ -759,7 +886,7 @@ class OpenStackCinderShell(object):
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except DiscoveryFailure:
except keystoneclient_exc.DiscoveryFailure:
# Discovery response mismatch. Raise the error
raise
except Exception:

View File

@@ -1,63 +0,0 @@
# 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 logging
import fixtures
import cinderclient.client
import cinderclient.v1.client
import cinderclient.v2.client
from cinderclient.tests import utils
class ClientTest(utils.TestCase):
def test_get_client_class_v1(self):
output = cinderclient.client.get_client_class('1')
self.assertEqual(cinderclient.v1.client.Client, output)
def test_get_client_class_v2(self):
output = cinderclient.client.get_client_class('2')
self.assertEqual(cinderclient.v2.client.Client, output)
def test_get_client_class_unknown(self):
self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
cinderclient.client.get_client_class, '0')
def test_log_req(self):
self.logger = self.useFixture(
fixtures.FakeLogger(
format="%(message)s",
level=logging.DEBUG,
nuke_handlers=True
)
)
kwargs = {}
kwargs['headers'] = {"X-Foo": "bar"}
kwargs['data'] = ('{"auth": {"tenantName": "fakeService",'
' "passwordCredentials": {"username": "fakeUser",'
' "password": "fakePassword"}}}')
cs = cinderclient.client.HTTPClient("user", None, None,
"http://127.0.0.1:5000")
cs.http_log_debug = True
cs.http_log_req('PUT', kwargs)
output = self.logger.output.split('\n')
print("JSBRYANT: output is", output)
self.assertNotIn("fakePassword", output[1])
self.assertIn("fakeUser", output[1])

View File

@@ -1,154 +0,0 @@
# 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 argparse
import re
import sys
import fixtures
import requests_mock
from six import moves
from testtools import matchers
from cinderclient import exceptions
from cinderclient import shell
from cinderclient.tests import utils
from cinderclient.tests.fixture_data import keystone_client
from keystoneclient.exceptions import DiscoveryFailure
class ShellTest(utils.TestCase):
FAKE_ENV = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where',
}
# Patch os.environ to avoid required auth info.
def setUp(self):
super(ShellTest, self).setUp()
for var in self.FAKE_ENV:
self.useFixture(fixtures.EnvironmentVariable(var,
self.FAKE_ENV[var]))
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = moves.StringIO()
_shell = shell.OpenStackCinderShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertEqual(0, exc_value.code)
finally:
out = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
return out
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'.*?^usage: ',
'.*?(?m)^\s+create\s+Creates a volume.',
'.*?(?m)^Run "cinder help SUBCOMMAND" for help on a subcommand.',
]
help_text = self.shell('help')
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_on_subcommand(self):
required = [
'.*?^usage: cinder list',
'.*?(?m)^Lists all volumes.',
]
help_text = self.shell('help list')
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def register_keystone_auth_fixture(self, mocker, url):
mocker.register_uri('GET', url,
text=keystone_client.keystone_request_callback)
@requests_mock.Mocker()
def test_version_discovery(self, mocker):
_shell = shell.OpenStackCinderShell()
os_auth_url = "https://WrongDiscoveryResponse.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
self.assertRaises(DiscoveryFailure, _shell._discover_auth_versions,
None, auth_url=os_auth_url)
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v2_url, os_auth_url, "Expected v2 url")
self.assertEqual(v3_url, None, "Expected no v3 url")
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v3.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v3_url, os_auth_url, "Expected v3 url")
self.assertEqual(v2_url, None, "Expected no v2 url")
class CinderClientArgumentParserTest(utils.TestCase):
def test_ambiguity_solved_for_one_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest='visible_param',
action='store_true')
parser.add_argument('--test_parameter',
dest='hidden_param',
action='store_true',
help=argparse.SUPPRESS)
opts = parser.parse_args(['--test'])
# visible argument must be set
self.assertTrue(opts.visible_param)
self.assertFalse(opts.hidden_param)
def test_raise_ambiguity_error_two_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="visible_param1",
action='store_true')
parser.add_argument('--test_parameter',
dest="visible_param2",
action='store_true')
self.assertRaises(SystemExit, parser.parse_args, ['--test'])
def test_raise_ambiguity_error_two_hidden_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="hidden_param1",
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--test_parameter',
dest="hidden_param2",
action='store_true',
help=argparse.SUPPRESS)
self.assertRaises(SystemExit, parser.parse_args, ['--test'])

View File

@@ -22,7 +22,10 @@ places where actual behavior differs from the spec.
from __future__ import print_function
def assert_has_keys(dict, required=[], optional=[]):
def assert_has_keys(dict, required=None, optional=None):
required = required or []
optional = optional or []
for k in required:
try:
assert k in dict

View File

@@ -11,7 +11,7 @@
# under the License.
from datetime import datetime
from cinderclient.tests.fixture_data import base
from cinderclient.tests.unit.fixture_data import base
# FIXME(jamielennox): use timeutils from oslo
FORMAT = '%Y-%m-%d %H:%M:%S'

View File

@@ -14,6 +14,42 @@ import fixtures
IDENTITY_URL = 'http://identityserver:5000/v2.0'
VOLUME_URL = 'http://volume.host'
TENANT_ID = 'b363706f891f48019483f8bd6503c54b'
VOLUME_V1_URL = '%(volume_url)s/v1/%(tenant_id)s' % {'volume_url': VOLUME_URL,
'tenant_id': TENANT_ID}
VOLUME_V2_URL = '%(volume_url)s/v2/%(tenant_id)s' % {'volume_url': VOLUME_URL,
'tenant_id': TENANT_ID}
def generate_version_output(v1=True, v2=True):
v1_dict = {
"status": "SUPPORTED",
"updated": "2014-06-28T12:20:21Z",
"id": "v1.0",
"links": [{
"href": "http://127.0.0.1:8776/v1/",
"rel": "self"
}]
}
v2_dict = {
"status": "CURRENT",
"updated": "2012-11-21T11:33:21Z",
"id": "v2.0", "links": [{
"href": "http://127.0.0.1:8776/v2/",
"rel": "self"
}]
}
versions = []
if v1:
versions.append(v1_dict)
if v2:
versions.append(v2_dict)
return {"versions": versions}
class Fixture(fixtures.Fixture):

View File

@@ -12,7 +12,7 @@
from keystoneclient import fixture
from cinderclient.tests.fixture_data import base
from cinderclient.tests.unit.fixture_data import base
from cinderclient.v1 import client as v1client
from cinderclient.v2 import client as v2client

View File

@@ -12,7 +12,7 @@
import json
from cinderclient.tests.fixture_data import base
from cinderclient.tests.unit.fixture_data import base
def _stub_snapshot(**kwargs):

View File

@@ -25,7 +25,7 @@ except ImportError:
from cinderclient import auth_plugin
from cinderclient import exceptions
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
from cinderclient.v1 import client

View File

@@ -14,8 +14,8 @@
from cinderclient import base
from cinderclient import exceptions
from cinderclient.v1 import volumes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -0,0 +1,195 @@
# 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 logging
import json
import fixtures
import mock
import cinderclient.client
import cinderclient.v1.client
import cinderclient.v2.client
from cinderclient import exceptions
from cinderclient.tests.unit.fixture_data import base as fixture_base
from cinderclient.tests.unit import utils
from keystoneclient import adapter
from keystoneclient import exceptions as keystone_exception
class ClientTest(utils.TestCase):
def test_get_client_class_v1(self):
output = cinderclient.client.get_client_class('1')
self.assertEqual(cinderclient.v1.client.Client, output)
def test_get_client_class_v2(self):
output = cinderclient.client.get_client_class('2')
self.assertEqual(cinderclient.v2.client.Client, output)
def test_get_client_class_unknown(self):
self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
cinderclient.client.get_client_class, '0')
def test_log_req(self):
self.logger = self.useFixture(
fixtures.FakeLogger(
format="%(message)s",
level=logging.DEBUG,
nuke_handlers=True
)
)
kwargs = {}
kwargs['headers'] = {"X-Foo": "bar"}
kwargs['data'] = ('{"auth": {"tenantName": "fakeService",'
' "passwordCredentials": {"username": "fakeUser",'
' "password": "fakePassword"}}}')
cs = cinderclient.client.HTTPClient("user", None, None,
"http://127.0.0.1:5000")
cs.http_log_debug = True
cs.http_log_req('PUT', kwargs)
output = self.logger.output.split('\n')
self.assertNotIn("fakePassword", output[1])
self.assertIn("fakeUser", output[1])
def test_versions(self):
v1_url = fixture_base.VOLUME_V1_URL
v2_url = fixture_base.VOLUME_V2_URL
unknown_url = 'http://fakeurl/v9/tenants'
self.assertEqual('1',
cinderclient.client.get_volume_api_from_url(v1_url))
self.assertEqual('2',
cinderclient.client.get_volume_api_from_url(v2_url))
self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
cinderclient.client.get_volume_api_from_url,
unknown_url)
@mock.patch.object(adapter.Adapter, 'request')
@mock.patch.object(exceptions, 'from_response')
def test_sessionclient_request_method(
self, mock_from_resp, mock_request):
kwargs = {
"body": {
"volume": {
"status": "creating",
"imageRef": "username",
"attach_status": "detached"
},
"authenticated": "True"
}
}
resp = {
"text": {
"volume": {
"status": "creating",
"id": "431253c0-e203-4da2-88df-60c756942aaf",
"size": 1
}
},
"code": 202
}
mock_response = utils.TestResponse({
"status_code": 202,
"text": json.dumps(resp),
})
# 'request' method of Adaptor will return 202 response
mock_request.return_value = mock_response
mock_session = mock.Mock()
mock_session.get_endpoint.return_value = fixture_base.VOLUME_V1_URL
session_client = cinderclient.client.SessionClient(
session=mock_session)
response, body = session_client.request(fixture_base.VOLUME_V1_URL,
'POST', **kwargs)
# In this case, from_response method will not get called
# because response status_code is < 400
self.assertEqual(202, response.status_code)
self.assertFalse(mock_from_resp.called)
@mock.patch.object(adapter.Adapter, 'request')
def test_sessionclient_request_method_raises_badrequest(
self, mock_request):
kwargs = {
"body": {
"volume": {
"status": "creating",
"imageRef": "username",
"attach_status": "detached"
},
"authenticated": "True"
}
}
resp = {
"badRequest": {
"message": "Invalid image identifier or unable to access "
"requested image.",
"code": 400
}
}
mock_response = utils.TestResponse({
"status_code": 400,
"text": json.dumps(resp),
})
# 'request' method of Adaptor will return 400 response
mock_request.return_value = mock_response
mock_session = mock.Mock()
mock_session.get_endpoint.return_value = fixture_base.VOLUME_V1_URL
session_client = cinderclient.client.SessionClient(
session=mock_session)
# 'from_response' method will raise BadRequest because
# resp.status_code is 400
self.assertRaises(exceptions.BadRequest, session_client.request,
fixture_base.VOLUME_V1_URL, 'POST', **kwargs)
@mock.patch.object(exceptions, 'from_response')
def test_keystone_request_raises_auth_failure_exception(
self, mock_from_resp):
kwargs = {
"body": {
"volume": {
"status": "creating",
"imageRef": "username",
"attach_status": "detached"
},
"authenticated": "True"
}
}
with mock.patch.object(adapter.Adapter, 'request',
side_effect=
keystone_exception.AuthorizationFailure()):
mock_session = mock.Mock()
mock_session.get_endpoint.return_value = fixture_base.VOLUME_V1_URL
session_client = cinderclient.client.SessionClient(
session=mock_session)
self.assertRaises(keystone_exception.AuthorizationFailure,
session_client.request,
fixture_base.VOLUME_V1_URL, 'POST', **kwargs)
# As keystonesession.request method will raise
# AuthorizationFailure exception, check exceptions.from_response
# is not getting called.
self.assertFalse(mock_from_resp.called)

View File

@@ -17,7 +17,7 @@ import requests
from cinderclient import client
from cinderclient import exceptions
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
fake_response = utils.TestResponse({
@@ -54,6 +54,9 @@ bad_500_request = mock.Mock(return_value=(bad_500_response))
connection_error_request = mock.Mock(
side_effect=requests.exceptions.ConnectionError)
timeout_error_request = mock.Mock(
side_effect=requests.exceptions.Timeout)
def get_client(retries=0):
cl = client.HTTPClient("username", "password",
@@ -68,6 +71,14 @@ def get_authed_client(retries=0):
return cl
def get_authed_bypass_url(retries=0):
cl = client.HTTPClient("username", "password",
"project_id", "auth_test",
bypass_url="volume/v100/", retries=retries)
cl.auth_token = "token"
return cl
class ClientTest(utils.TestCase):
def test_get(self):
@@ -198,6 +209,10 @@ class ClientTest(utils.TestCase):
test_get_call()
self.assertEqual([], self.requests)
def test_get_no_auth_url(self):
client.HTTPClient("username", "password",
"project_id", retries=0)
def test_post(self):
cl = get_authed_client()
@@ -220,6 +235,11 @@ class ClientTest(utils.TestCase):
test_post_call()
def test_bypass_url(self):
cl = get_authed_bypass_url()
self.assertEqual("volume/v100", cl.bypass_url)
self.assertEqual("volume/v100", cl.management_url)
def test_auth_failure(self):
cl = get_client()
@@ -241,3 +261,20 @@ class ClientTest(utils.TestCase):
self.assertRaises(NotImplementedError, cl.authenticate)
test_auth_call()
def test_get_retry_timeout_error(self):
cl = get_authed_client(retries=1)
self.requests = [timeout_error_request, mock_request]
def request(*args, **kwargs):
next_request = self.requests.pop(0)
return next_request(*args, **kwargs)
@mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call():
resp, body = cl.get("/hi")
test_get_call()
self.assertEqual(self.requests, [])

View File

@@ -13,7 +13,7 @@
from cinderclient import exceptions
from cinderclient import service_catalog
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
# Taken directly from keystone/content/common/samples/auth.json

View File

@@ -0,0 +1,368 @@
# 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 argparse
import re
import sys
import fixtures
from keystoneclient import fixture as keystone_client_fixture
import mock
import requests_mock
from six import moves
from testtools import matchers
from cinderclient import exceptions
from cinderclient import shell
from cinderclient.tests.unit.fixture_data import base as fixture_base
from cinderclient.tests.unit.fixture_data import keystone_client
from cinderclient.tests.unit import utils
import keystoneclient.exceptions as ks_exc
from keystoneclient.exceptions import DiscoveryFailure
class ShellTest(utils.TestCase):
FAKE_ENV = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': '%s/v2.0' % keystone_client.BASE_HOST,
}
# Patch os.environ to avoid required auth info.
def make_env(self, exclude=None):
env = dict((k, v) for k, v in self.FAKE_ENV.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
def setUp(self):
super(ShellTest, self).setUp()
for var in self.FAKE_ENV:
self.useFixture(fixtures.EnvironmentVariable(var,
self.FAKE_ENV[var]))
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = moves.StringIO()
_shell = shell.OpenStackCinderShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertEqual(0, exc_value.code)
finally:
out = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
return out
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'.*?^usage: ',
'.*?(?m)^\s+create\s+Creates a volume.',
'.*?(?m)^Run "cinder help SUBCOMMAND" for help on a subcommand.',
]
help_text = self.shell('help')
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_on_subcommand(self):
required = [
'.*?^usage: cinder list',
'.*?(?m)^Lists all volumes.',
]
help_text = self.shell('help list')
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def register_keystone_auth_fixture(self, mocker, url):
mocker.register_uri('GET', url,
text=keystone_client.keystone_request_callback)
@requests_mock.Mocker()
def test_version_discovery(self, mocker):
_shell = shell.OpenStackCinderShell()
os_auth_url = "https://WrongDiscoveryResponse.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
self.assertRaises(DiscoveryFailure, _shell._discover_auth_versions,
None, auth_url=os_auth_url)
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v2_url, os_auth_url, "Expected v2 url")
self.assertEqual(v3_url, None, "Expected no v3 url")
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v3.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v3_url, os_auth_url, "Expected v3 url")
self.assertEqual(v2_url, None, "Expected no v2 url")
@requests_mock.Mocker()
def test_cinder_version_legacy_endpoint_v1_and_v2(self, mocker):
"""Verify that legacy endpoint settings still work.
Legacy endpoints that are not using version discovery is
<hostname>:<port>/<version>/(tenant_id)s. For this unit test, we fill
in the tenant_id for mocking purposes.
"""
token = keystone_client_fixture.V2Token()
cinder_url = 'http://127.0.0.1:8776/v1/%s' % fixture_base.TENANT_ID
volume_service = token.add_service('volume', 'Cinder v1')
volume_service.add_endpoint(public=cinder_url, region='RegionOne')
volumev2_service = token.add_service('volumev2', 'Cinder v2')
volumev2_service.add_endpoint(public=cinder_url, region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(cinder_url, json=fixture_base.generate_version_output())
volume_request = mocker.get('http://127.0.0.1:8776/v1/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_legacy_endpoint_only_v1(self, mocker):
"""Verify that v1 legacy endpoint settings still work.
Legacy endpoints that are not using version discovery is
<hostname>:<port>/<version>/(tenant_id)s. For this unit test, we fill
in the tenant_id for mocking purposes.
"""
token = keystone_client_fixture.V2Token()
cinder_url = 'http://127.0.0.1:8776/v1/%s' % fixture_base.TENANT_ID
volume_service = token.add_service('volume', 'Cinder v1')
volume_service.add_endpoint(
public=cinder_url,
region='RegionOne'
)
mocker.get(
cinder_url,
json=fixture_base.generate_version_output(v1=True, v2=False)
)
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
volume_request = mocker.get('http://127.0.0.1:8776/v1/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_legacy_endpoint_only_v2(self, mocker):
"""Verify that v2 legacy endpoint settings still work.
Legacy endpoints that are not using version discovery is
<hostname>:<port>/<version>/(tenant_id)s. For this unit test, we fill
in the tenant_id for mocking purposes.
"""
token = keystone_client_fixture.V2Token()
cinder_url = 'http://127.0.0.1:8776/v2/%s' % fixture_base.TENANT_ID
volumev2_service = token.add_service('volumev2', 'Cinder v2')
volumev2_service.add_endpoint(
public=cinder_url,
region='RegionOne'
)
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
cinder_url,
json=fixture_base.generate_version_output(v1=False, v2=True)
)
volume_request = mocker.get('http://127.0.0.1:8776/v2/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_discovery(self, mocker):
"""Verify client works two endpoints enabled under one service."""
token = keystone_client_fixture.V2Token()
volume_service = token.add_service('volume', 'Cinder')
volume_service.add_endpoint(public='http://127.0.0.1:8776',
region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
'http://127.0.0.1:8776/',
json=fixture_base.generate_version_output(v1=True, v2=True)
)
v1_request = mocker.get('http://127.0.0.1:8776/v1/volumes/detail',
json={'volumes': {}})
v2_request = mocker.get('http://127.0.0.1:8776/v2/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(v1_request.called)
self.shell('--os-volume-api-version 2 list')
self.assertTrue(v2_request.called)
@requests_mock.Mocker()
def test_cinder_version_discovery_only_v1(self, mocker):
"""Verify when v1 is only enabled, the client discovers it."""
token = keystone_client_fixture.V2Token()
volume_service = token.add_service('volume', 'Cinder')
volume_service.add_endpoint(public='http://127.0.0.1:8776',
region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
'http://127.0.0.1:8776/',
json=fixture_base.generate_version_output(v1=True, v2=True)
)
volume_request = mocker.get('http://127.0.0.1:8776/v1/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_discovery_only_v2(self, mocker):
"""Verify when v2 is enabled, the client discovers it."""
token = keystone_client_fixture.V2Token()
volumev2_service = token.add_service('volume', 'Cinder')
volumev2_service.add_endpoint(public='http://127.0.0.1:8776',
region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
'http://127.0.0.1:8776/',
json=fixture_base.generate_version_output(v1=False, v2=True)
)
volume_request = mocker.get('http://127.0.0.1:8776/v2/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_discovery_fallback(self, mocker):
"""Client defaults to v1, but v2 is only available, fallback to v2."""
token = keystone_client_fixture.V2Token()
volumev2_service = token.add_service('volumev2', 'Cinder v2')
volumev2_service.add_endpoint(public='http://127.0.0.1:8776',
region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
'http://127.0.0.1:8776/',
json=fixture_base.generate_version_output(v1=False, v2=True)
)
volume_request = mocker.get('http://127.0.0.1:8776/v2/volumes/detail',
json={'volumes': {}})
self.shell('list')
self.assertTrue(volume_request.called)
@requests_mock.Mocker()
def test_cinder_version_discovery_unsupported_version(self, mocker):
"""Try a version from the client that's not enabled in Cinder."""
token = keystone_client_fixture.V2Token()
volume_service = token.add_service('volume', 'Cinder')
volume_service.add_endpoint(public='http://127.0.0.1:8776',
region='RegionOne')
mocker.post(keystone_client.BASE_HOST + '/v2.0/tokens',
json=token)
mocker.get(
'http://127.0.0.1:8776/',
json=fixture_base.generate_version_output(v1=False, v2=True)
)
self.assertRaises(exceptions.InvalidAPIVersion,
self.shell, '--os-volume-api-version 1 list')
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
@mock.patch('getpass.getpass', return_value='password')
def test_password_prompted(self, mock_getpass, mock_stdin):
self.make_env(exclude='OS_PASSWORD')
# We should get a Connection Refused because there is no keystone.
self.assertRaises(ks_exc.ConnectionRefused, self.shell, 'list')
# Make sure we are actually prompted.
mock_getpass.assert_called_with('OS Password: ')
class CinderClientArgumentParserTest(utils.TestCase):
def test_ambiguity_solved_for_one_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest='visible_param',
action='store_true')
parser.add_argument('--test_parameter',
dest='hidden_param',
action='store_true',
help=argparse.SUPPRESS)
opts = parser.parse_args(['--test'])
# visible argument must be set
self.assertTrue(opts.visible_param)
self.assertFalse(opts.hidden_param)
def test_raise_ambiguity_error_two_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="visible_param1",
action='store_true')
parser.add_argument('--test_parameter',
dest="visible_param2",
action='store_true')
self.assertRaises(SystemExit, parser.parse_args, ['--test'])
def test_raise_ambiguity_error_two_hidden_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="hidden_param1",
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--test_parameter',
dest="hidden_param2",
action='store_true',
help=argparse.SUPPRESS)
self.assertRaises(SystemExit, parser.parse_args, ['--test'])

View File

@@ -19,7 +19,7 @@ from six import moves
from cinderclient import exceptions
from cinderclient import utils
from cinderclient import base
from cinderclient.tests import utils as test_utils
from cinderclient.tests.unit import utils as test_utils
UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
@@ -93,7 +93,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 statements in its block."""
def __enter__(self):
self.real_stdout = sys.stdout
self.stringio = moves.StringIO()
@@ -110,9 +110,10 @@ 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)]
to_print = [Row(a=3, b=4), Row(a=1, b=2)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'])
# Output should be sorted by the first key (a)
self.assertEqual("""\
+---+---+
| a | b |
@@ -120,6 +121,51 @@ class PrintListTestCase(test_utils.TestCase):
| 1 | 2 |
| 3 | 4 |
+---+---+
""", cso.read())
def test_print_list_with_None_data(self):
Row = collections.namedtuple('Row', ['a', 'b'])
to_print = [Row(a=3, b=None), Row(a=1, b=2)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'])
# Output should be sorted by the first key (a)
self.assertEqual("""\
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 3 | - |
+---+---+
""", cso.read())
def test_print_list_with_list_sortby(self):
Row = collections.namedtuple('Row', ['a', 'b'])
to_print = [Row(a=4, b=3), Row(a=2, b=1)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'], sortby_index=1)
# Output should be sorted by the second key (b)
self.assertEqual("""\
+---+---+
| a | b |
+---+---+
| 2 | 1 |
| 4 | 3 |
+---+---+
""", cso.read())
def test_print_list_with_list_no_sort(self):
Row = collections.namedtuple('Row', ['a', 'b'])
to_print = [Row(a=3, b=4), Row(a=1, b=2)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'], sortby_index=None)
# Output should be in the order given
self.assertEqual("""\
+---+---+
| a | b |
+---+---+
| 3 | 4 |
| 1 | 2 |
+---+---+
""", cso.read())
def test_print_list_with_generator(self):

View File

@@ -14,8 +14,8 @@
from cinderclient import extension
from cinderclient.v1.contrib import list_extensions
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
extensions = [

View File

@@ -21,8 +21,8 @@ except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.tests.unit import fakes
import cinderclient.tests.unit.utils as utils
from cinderclient.v1 import client
@@ -385,7 +385,9 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1}})
def get_os_quota_sets_test_defaults(self):
return (200, {}, {'quota_set': {
@@ -393,7 +395,9 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert list(body) == ['quota_set']
@@ -404,7 +408,9 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1}})
def delete_os_quota_sets_1234(self, **kw):
return (200, {}, {})
@@ -422,7 +428,9 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
@@ -433,7 +441,9 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1}})
#
# VolumeTypes

View File

@@ -18,7 +18,7 @@ import requests
from cinderclient.v1 import client
from cinderclient import exceptions
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):

View File

@@ -18,9 +18,9 @@ import six
from cinderclient.v1 import availability_zones
from cinderclient.v1 import shell
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import availability_zones as azfixture
from cinderclient.tests import utils
from cinderclient.tests.unit.fixture_data import client
from cinderclient.tests.unit.fixture_data import availability_zones as azfixture # noqa
from cinderclient.tests.unit import utils
class AvailabilityZoneTest(utils.FixturedTestCase):

View File

@@ -15,7 +15,7 @@
import mock
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
from cinderclient.v1 import limits

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
@@ -29,14 +29,31 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2, snapshots=2)
q.update(volumes=2, snapshots=2, gigabytes=2000,
backups=2, backup_gigabytes=2000)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):
q = cs.quota_classes.get('test')
q2 = cs.quota_classes.get('test')
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
q2.volumes = 0
self.assertNotEqual(q.volumes, q2.volumes)
q2.snapshots = 0
self.assertNotEqual(q.snapshots, q2.snapshots)
q2.gigabytes = 0
self.assertNotEqual(q.gigabytes, q2.gigabytes)
q2.backups = 0
self.assertNotEqual(q.backups, q2.backups)
q2.backup_gigabytes = 0
self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes)
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
@@ -36,6 +36,7 @@ class QuotaSetsTest(utils.TestCase):
q = cs.quotas.get('test')
q.update(volumes=2)
q.update(snapshots=2)
q.update(backups=2)
cs.assert_called('PUT', '/os-quota-sets/test')
def test_refresh_quota(self):
@@ -43,13 +44,17 @@ class QuotaSetsTest(utils.TestCase):
q2 = cs.quotas.get('test')
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.backups, q2.backups)
q2.volumes = 0
self.assertNotEqual(q.volumes, q2.volumes)
q2.snapshots = 0
self.assertNotEqual(q.snapshots, q2.snapshots)
q2.backups = 0
self.assertNotEqual(q.backups, q2.backups)
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.backups, q2.backups)
def test_delete_quota(self):
tenant_id = 'test'

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
from cinderclient.v1 import services

View File

@@ -16,15 +16,17 @@
# under the License.
import fixtures
from keystoneclient import fixture as keystone_client_fixture
from requests_mock.contrib import fixture as requests_mock_fixture
from cinderclient import client
from cinderclient import exceptions
from cinderclient import shell
from cinderclient.tests.unit.fixture_data import base as fixture_base
from cinderclient.tests.unit.fixture_data import keystone_client
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
from cinderclient.v1 import shell as shell_v1
from cinderclient.tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.fixture_data import keystone_client
class ShellTest(utils.TestCase):
@@ -54,7 +56,18 @@ class ShellTest(utils.TestCase):
self.requests = self.useFixture(requests_mock_fixture.Fixture())
self.requests.register_uri(
'GET', keystone_client.BASE_URL,
text=keystone_client.keystone_request_callback)
text=keystone_client.keystone_request_callback
)
token = keystone_client_fixture.V2Token()
s = token.add_service('volume', 'cinder')
s.add_endpoint(public='http://127.0.0.1:8776')
self.requests.post(keystone_client.BASE_URL + 'v2.0/tokens',
json=token)
self.requests.get(
'http://127.0.0.1:8776',
json=fixture_base.generate_version_output()
)
def tearDown(self):
# For some method like test_image_meta_bad_action we are
@@ -81,8 +94,8 @@ class ShellTest(utils.TestCase):
# mimic the result of argparse's parse_args() method
class Arguments:
def __init__(self, metadata=[]):
self.metadata = metadata
def __init__(self, metadata=None):
self.metadata = metadata or []
inputs = [
([], {}),
@@ -112,6 +125,85 @@ class ShellTest(utils.TestCase):
# NOTE(jdg): we default to detail currently
self.assert_called('GET', '/volumes/detail')
def test_list_filter_tenant_with_all_tenants(self):
self.run_command('list --tenant=123 --all-tenants 1')
self.assert_called('GET',
'/volumes/detail?all_tenants=1&project_id=123')
def test_list_filter_tenant_without_all_tenants(self):
self.run_command('list --tenant=123')
self.assert_called('GET',
'/volumes/detail?all_tenants=1&project_id=123')
def test_metadata_args_with_limiter(self):
self.run_command('create --metadata key1="--test1" 1')
expected = {'volume': {'snapshot_id': None,
'display_description': None,
'source_volid': None,
'status': 'creating',
'size': 1,
'volume_type': None,
'imageRef': None,
'availability_zone': None,
'attach_status': 'detached',
'user_id': None,
'project_id': None,
'metadata': {'key1': '"--test1"'},
'display_name': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_metadata_args_limiter_display_name(self):
self.run_command('create --metadata key1="--t1" --display-name="t" 1')
expected = {'volume': {'snapshot_id': None,
'display_description': None,
'source_volid': None,
'status': 'creating',
'size': 1,
'volume_type': None,
'imageRef': None,
'availability_zone': None,
'attach_status': 'detached',
'user_id': None,
'project_id': None,
'metadata': {'key1': '"--t1"'},
'display_name': '"t"'}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_delimit_metadata_args(self):
self.run_command('create --metadata key1="test1" key2="test2" 1')
expected = {'volume': {'snapshot_id': None,
'display_description': None,
'source_volid': None,
'status': 'creating',
'size': 1,
'volume_type': None,
'imageRef': None,
'availability_zone': None,
'attach_status': 'detached',
'user_id': None,
'project_id': None,
'metadata': {'key1': '"test1"',
'key2': '"test2"'},
'display_name': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_delimit_metadata_args_display_name(self):
self.run_command('create --metadata key1="t1" --display-name="t" 1')
expected = {'volume': {'snapshot_id': None,
'display_description': None,
'source_volid': None,
'status': 'creating',
'size': 1,
'volume_type': None,
'imageRef': None,
'availability_zone': None,
'attach_status': 'detached',
'user_id': None,
'project_id': None,
'metadata': {'key1': '"t1"'},
'display_name': '"t"'}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_list_filter_status(self):
self.run_command('list --status=available')
self.assert_called('GET', '/volumes/detail?status=available')
@@ -226,6 +318,11 @@ class ShellTest(utils.TestCase):
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_attach(self):
self.run_command('reset-state --state in-use 1234')
expected = {'os-reset_status': {'status': 'in-use'}}
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'}}
@@ -400,3 +497,11 @@ class ShellTest(utils.TestCase):
def test_snapshot_delete_multiple(self):
self.run_command('snapshot-delete 1234 5678')
self.assert_called('DELETE', '/snapshots/5678')
def test_list_transfer(self):
self.run_command('transfer-list')
self.assert_called('GET', '/os-volume-transfer/detail')
def test_list_transfer_all_tenants(self):
self.run_command('transfer-list --all-tenants=1')
self.assert_called('GET', '/os-volume-transfer/detail?all_tenants=1')

View File

@@ -13,9 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import snapshots
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.fixture_data import client
from cinderclient.tests.unit.fixture_data import snapshots
class SnapshotActionsTest(utils.FixturedTestCase):

View File

@@ -12,8 +12,8 @@
# limitations under the License.
from cinderclient.v1 import volume_types
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -14,8 +14,8 @@
# under the License.
from cinderclient.v1.volume_encryption_types import VolumeEncryptionType
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -11,8 +11,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -16,8 +16,8 @@
from cinderclient import extension
from cinderclient.v2.contrib import list_extensions
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
extensions = [

View File

@@ -20,8 +20,8 @@ except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.tests.unit import fakes
import cinderclient.tests.unit.utils as utils
from cinderclient.v2 import client
@@ -69,32 +69,49 @@ def _stub_snapshot(**kwargs):
return snapshot
def _stub_consistencygroup(**kwargs):
def _stub_consistencygroup(detailed=True, **kwargs):
consistencygroup = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": "cg",
"id": "11111111-1111-1111-1111-111111111111",
"availability_zone": "myzone",
"status": "available",
}
if detailed:
details = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"availability_zone": "myzone",
"status": "available",
}
consistencygroup.update(details)
consistencygroup.update(kwargs)
return consistencygroup
def _stub_cgsnapshot(**kwargs):
def _stub_cgsnapshot(detailed=True, **kwargs):
cgsnapshot = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": None,
"id": "11111111-1111-1111-1111-111111111111",
"status": "available",
"consistencygroup_id": "00000000-0000-0000-0000-000000000000",
}
if detailed:
details = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": None,
"id": "11111111-1111-1111-1111-111111111111",
"status": "available",
"consistencygroup_id": "00000000-0000-0000-0000-000000000000",
}
cgsnapshot.update(details)
cgsnapshot.update(kwargs)
return cgsnapshot
def _stub_type_access(**kwargs):
access = {'volume_type_id': '11111111-1111-1111-1111-111111111111',
'project_id': '00000000-0000-0000-0000-000000000000'}
access.update(kwargs)
return access
def _self_href(base_uri, tenant_id, backup_id):
return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id)
@@ -239,6 +256,8 @@ class FakeHTTPClient(base_client.HTTPClient):
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v2/fake'
self.osapi_max_limit = 1000
self.marker = None
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
@@ -250,7 +269,16 @@ class FakeHTTPClient(base_client.HTTPClient):
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
url_split = url.rsplit('?', 1)
munged_url = url_split[0]
if len(url_split) > 1:
parameters = url_split[1]
if 'marker' in parameters:
self.marker = int(parameters.rsplit('marker=', 1)[1])
else:
self.marker = None
else:
self.marker = None
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
@@ -296,6 +324,13 @@ class FakeHTTPClient(base_client.HTTPClient):
def get_snapshots_5678(self, **kw):
return (200, {}, {'snapshot': _stub_snapshot(id='5678')})
def post_snapshots(self, **kw):
metadata = kw['body']['snapshot'].get('metadata', None)
snapshot = _stub_snapshot(id='1234', volume_id='1234')
if snapshot is not None:
snapshot.update({'metadata': metadata})
return (202, {}, {'snapshot': snapshot})
def put_snapshots_1234(self, **kw):
snapshot = _stub_snapshot(id='1234')
snapshot.update(kw['body']['snapshot'])
@@ -333,10 +368,21 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {}, {'volume': volume})
def get_volumes(self, **kw):
return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'},
{'id': 5678, 'name': 'sample-volume2'}
]})
if self.marker == 1234:
return (200, {}, {"volumes": [
{'id': 5678, 'name': 'sample-volume2'}
]})
elif self.osapi_max_limit == 1:
return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'}
], "volumes_links": [
{'href': "/volumes?limit=1&marker=1234", 'rel': 'next'}
]})
else:
return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'},
{'id': 5678, 'name': 'sample-volume2'}
]})
# TODO(jdg): This will need to change
# at the very least it's not complete
@@ -429,6 +475,11 @@ class FakeHTTPClient(base_client.HTTPClient):
_stub_consistencygroup(id='1234'),
_stub_consistencygroup(id='4567')]})
def get_consistencygroups(self, **kw):
return (200, {}, {"consistencygroups": [
_stub_consistencygroup(detailed=False, id='1234'),
_stub_consistencygroup(detailed=False, id='4567')]})
def get_consistencygroups_1234(self, **kw):
return (200, {}, {'consistencygroup':
_stub_consistencygroup(id='1234')})
@@ -436,6 +487,9 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_consistencygroups(self, **kw):
return (202, {}, {'consistencygroup': {}})
def put_consistencygroups_1234(self, **kw):
return (200, {}, {'consistencygroup': {}})
def post_consistencygroups_1234_delete(self, **kw):
return (202, {}, {})
@@ -448,12 +502,20 @@ class FakeHTTPClient(base_client.HTTPClient):
_stub_cgsnapshot(id='1234'),
_stub_cgsnapshot(id='4567')]})
def get_cgsnapshots(self, **kw):
return (200, {}, {"cgsnapshots": [
_stub_cgsnapshot(detailed=False, id='1234'),
_stub_cgsnapshot(detailed=False, id='4567')]})
def get_cgsnapshots_1234(self, **kw):
return (200, {}, {'cgsnapshot': _stub_cgsnapshot(id='1234')})
def post_cgsnapshots(self, **kw):
return (202, {}, {'cgsnapshot': {}})
def put_cgsnapshots_1234(self, **kw):
return (200, {}, {'cgsnapshot': {}})
def delete_cgsnapshots_1234(self, **kw):
return (202, {}, {})
@@ -467,7 +529,10 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
'consistencygroups': 1}})
def get_os_quota_sets_test_defaults(self):
return (200, {}, {'quota_set': {
@@ -475,7 +540,10 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
'consistencygroups': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert list(body) == ['quota_set']
@@ -486,7 +554,10 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
'consistencygroups': 2}})
def delete_os_quota_sets_1234(self, **kw):
return (200, {}, {})
@@ -504,7 +575,10 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
'consistencygroups': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
@@ -515,35 +589,67 @@ class FakeHTTPClient(base_client.HTTPClient):
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
'gigabytes': 1,
'backups': 1,
'backup_gigabytes': 1,
'consistencygroups': 2}})
#
# VolumeTypes
#
def get_types(self, **kw):
return (200, {}, {
'volume_types': [{'id': 1,
'name': 'test-type-1',
'description': 'test_type-1-desc',
'extra_specs': {}},
{'id': 2,
'name': 'test-type-2',
'description': 'test_type-2-desc',
'extra_specs': {}}]})
def get_types_1(self, **kw):
return (200, {}, {'volume_type': {'id': 1,
'name': 'test-type-1',
'description': 'test_type-1-desc',
'extra_specs': {}}})
def get_types_2(self, **kw):
return (200, {}, {'volume_type': {'id': 2,
'name': 'test-type-2',
'description': 'test_type-2-desc',
'extra_specs': {}}})
def get_types_3(self, **kw):
return (200, {}, {'volume_type': {'id': 3,
'name': 'test-type-3',
'description': 'test_type-3-desc',
'extra_specs': {},
'os-volume-type-access:is_public': False}})
def get_types_default(self, **kw):
return self.get_types_1()
def post_types(self, body, **kw):
return (202, {}, {'volume_type': {'id': 3,
'name': 'test-type-3',
'description': 'test_type-3-desc',
'extra_specs': {}}})
def post_types_3_action(self, body, **kw):
_body = None
resp = 202
assert len(list(body)) == 1
action = list(body)[0]
if action == 'addProjectAccess':
assert 'project' in body['addProjectAccess']
elif action == 'removeProjectAccess':
assert 'project' in body['removeProjectAccess']
else:
raise AssertionError('Unexpected action: %s' % action)
return (resp, {}, _body)
def post_types_1_extra_specs(self, body, **kw):
assert list(body) == ['extra_specs']
return (200, {}, {'extra_specs': {'k': 'v'}})
@@ -554,6 +660,18 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_types_1(self, **kw):
return (202, {}, None)
def put_types_1(self, **kw):
return self.get_types_1()
#
# VolumeAccess
#
def get_types_3_os_volume_type_access(self, **kw):
return (200, {}, {'volume_type_access': [
_stub_type_access()
]})
#
# VolumeEncryptionTypes
#
@@ -908,3 +1026,24 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_os_reenable_replica_1234(self, **kw):
return (202, {}, {})
def get_scheduler_stats_get_pools(self, **kw):
stats = [
{
"name": "ubuntu@lvm#backend_name",
"capabilities": {
"pool_name": "backend_name",
"QoS_support": False,
"timestamp": "2014-11-21T18:15:28.141161",
"allocated_capacity_gb": 0,
"volume_backend_name": "backend_name",
"free_capacity_gb": 7.01,
"driver_version": "2.0.0",
"total_capacity_gb": 10.01,
"reserved_percentage": 0,
"vendor_name": "Open Source",
"storage_protocol": "iSCSI",
}
},
]
return (200, {}, {"pools": stats})

View File

@@ -21,7 +21,7 @@ import requests
from cinderclient import exceptions
from cinderclient.v2 import client
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):

View File

@@ -18,9 +18,9 @@ import six
from cinderclient.v2 import availability_zones
from cinderclient.v2 import shell
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import availability_zones as azfixture
from cinderclient.tests import utils
from cinderclient.tests.unit.fixture_data import client
from cinderclient.tests.unit.fixture_data import availability_zones as azfixture # noqa
from cinderclient.tests.unit import utils
class AvailabilityZoneTest(utils.FixturedTestCase):

View File

@@ -14,8 +14,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
@@ -46,10 +46,35 @@ class cgsnapshotsTest(utils.TestCase):
'project_id': None}}
cs.assert_called('POST', '/cgsnapshots', body=expected)
def test_update_cgsnapshot(self):
v = cs.cgsnapshots.list()[0]
expected = {'cgsnapshot': {'name': 'cgs2'}}
v.update(name='cgs2')
cs.assert_called('PUT', '/cgsnapshots/1234', body=expected)
cs.cgsnapshots.update('1234', name='cgs2')
cs.assert_called('PUT', '/cgsnapshots/1234', body=expected)
cs.cgsnapshots.update(v, name='cgs2')
cs.assert_called('PUT', '/cgsnapshots/1234', body=expected)
def test_update_cgsnapshot_no_props(self):
cs.cgsnapshots.update('1234')
def test_list_cgsnapshot(self):
cs.cgsnapshots.list()
cs.assert_called('GET', '/cgsnapshots/detail')
def test_list_cgsnapshot_detailed_false(self):
cs.cgsnapshots.list(detailed=False)
cs.assert_called('GET', '/cgsnapshots')
def test_list_cgsnapshot_with_search_opts(self):
cs.cgsnapshots.list(search_opts={'foo': 'bar'})
cs.assert_called('GET', '/cgsnapshots/detail?foo=bar')
def test_list_cgsnapshot_with_empty_search_opt(self):
cs.cgsnapshots.list(search_opts={'foo': 'bar', '123': None})
cs.assert_called('GET', '/cgsnapshots/detail?foo=bar')
def test_get_cgsnapshot(self):
cgsnapshot_id = '1234'
cs.cgsnapshots.get(cgsnapshot_id)

View File

@@ -14,8 +14,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
@@ -47,6 +47,36 @@ class ConsistencygroupsTest(utils.TestCase):
'project_id': None}}
cs.assert_called('POST', '/consistencygroups', body=expected)
def test_update_consistencygroup(self):
v = cs.consistencygroups.list()[0]
expected = {'consistencygroup': {'name': 'cg2'}}
v.update(name='cg2')
cs.assert_called('PUT', '/consistencygroups/1234', body=expected)
cs.consistencygroups.update('1234', name='cg2')
cs.assert_called('PUT', '/consistencygroups/1234', body=expected)
cs.consistencygroups.update(v, name='cg2')
cs.assert_called('PUT', '/consistencygroups/1234', body=expected)
def test_update_consistencygroup_no_props(self):
cs.consistencygroups.update('1234')
def test_list_consistencygroup(self):
cs.consistencygroups.list()
cs.assert_called('GET', '/consistencygroups/detail')
def test_list_consistencygroup_detailed_false(self):
cs.consistencygroups.list(detailed=False)
cs.assert_called('GET', '/consistencygroups')
def test_list_consistencygroup_with_search_opts(self):
cs.consistencygroups.list(search_opts={'foo': 'bar'})
cs.assert_called('GET', '/consistencygroups/detail?foo=bar')
def test_list_consistencygroup_with_empty_search_opt(self):
cs.consistencygroups.list(search_opts={'foo': 'bar', 'abc': None})
cs.assert_called('GET', '/consistencygroups/detail?foo=bar')
def test_get_consistencygroup(self):
consistencygroup_id = '1234'
cs.consistencygroups.get(consistencygroup_id)
cs.assert_called('GET', '/consistencygroups/%s' % consistencygroup_id)

View File

@@ -15,7 +15,7 @@
import mock
from cinderclient.tests import utils
from cinderclient.tests.unit import utils
from cinderclient.v2 import limits

View File

@@ -0,0 +1,44 @@
# Copyright (C) 2015 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.v2.pools import Pool
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
class PoolsTest(utils.TestCase):
def test_get_pool_stats(self):
sl = cs.pools.list()
cs.assert_called('GET', '/scheduler-stats/get_pools')
for s in sl:
self.assertIsInstance(s, Pool)
self.assertTrue(hasattr(s, "name"))
self.assertFalse(hasattr(s, "capabilities"))
# basic list should not have volume_backend_name (or any other
# entries from capabilities)
self.assertFalse(hasattr(s, "volume_backend_name"))
def test_get_detail_pool_stats(self):
sl = cs.pools.list(detailed=True)
cs.assert_called('GET', '/scheduler-stats/get_pools?detail=True')
for s in sl:
self.assertIsInstance(s, Pool)
self.assertTrue(hasattr(s, "name"))
self.assertFalse(hasattr(s, "capabilities"))
# detail list should have a volume_backend_name (from capabilities)
self.assertTrue(hasattr(s, "volume_backend_name"))

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
@@ -29,14 +29,36 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2, snapshots=2)
q.update(volumes=2, snapshots=2, gigabytes=2000,
backups=2, backup_gigabytes=2000,
consistencygroups=2)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):
q = cs.quota_classes.get('test')
q2 = cs.quota_classes.get('test')
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
self.assertEqual(q.consistencygroups, q2.consistencygroups)
q2.volumes = 0
self.assertNotEqual(q.volumes, q2.volumes)
q2.snapshots = 0
self.assertNotEqual(q.snapshots, q2.snapshots)
q2.gigabytes = 0
self.assertNotEqual(q.gigabytes, q2.gigabytes)
q2.backups = 0
self.assertNotEqual(q.backups, q2.backups)
q2.backup_gigabytes = 0
self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes)
q2.consistencygroups = 0
self.assertNotEqual(q.consistencygroups, q2.consistencygroups)
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
self.assertEqual(q.consistencygroups, q2.consistencygroups)

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
@@ -36,6 +36,10 @@ class QuotaSetsTest(utils.TestCase):
q = cs.quotas.get('test')
q.update(volumes=2)
q.update(snapshots=2)
q.update(gigabytes=2000)
q.update(backups=2)
q.update(backup_gigabytes=2000)
q.update(consistencygroups=2)
cs.assert_called('PUT', '/os-quota-sets/test')
def test_refresh_quota(self):
@@ -43,13 +47,29 @@ class QuotaSetsTest(utils.TestCase):
q2 = cs.quotas.get('test')
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
self.assertEqual(q.consistencygroups, q2.consistencygroups)
q2.volumes = 0
self.assertNotEqual(q.volumes, q2.volumes)
q2.snapshots = 0
self.assertNotEqual(q.snapshots, q2.snapshots)
q2.gigabytes = 0
self.assertNotEqual(q.gigabytes, q2.gigabytes)
q2.backups = 0
self.assertNotEqual(q.backups, q2.backups)
q2.backup_gigabytes = 0
self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes)
q2.consistencygroups = 0
self.assertNotEqual(q.consistencygroups, q2.consistencygroups)
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
self.assertEqual(q.gigabytes, q2.gigabytes)
self.assertEqual(q.backups, q2.backups)
self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes)
self.assertEqual(q.consistencygroups, q2.consistencygroups)
def test_delete_quota(self):
tenant_id = 'test'

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
from cinderclient.v2 import services

View File

@@ -14,14 +14,18 @@
# under the License.
import fixtures
from keystoneclient import fixture as keystone_client_fixture
import mock
from requests_mock.contrib import fixture as requests_mock_fixture
from six.moves.urllib import parse
from cinderclient import client
from cinderclient import exceptions
from cinderclient import shell
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
from cinderclient.tests.fixture_data import keystone_client
from cinderclient.tests.unit.fixture_data import base as fixture_base
from cinderclient.tests.unit.fixture_data import keystone_client
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
class ShellTest(utils.TestCase):
@@ -51,12 +55,23 @@ class ShellTest(utils.TestCase):
self.requests = self.useFixture(requests_mock_fixture.Fixture())
self.requests.register_uri(
'GET', keystone_client.BASE_URL,
text=keystone_client.keystone_request_callback)
text=keystone_client.keystone_request_callback
)
token = keystone_client_fixture.V2Token()
s = token.add_service('volume', 'cinder')
s.add_endpoint(public='http://127.0.0.1:8776')
self.requests.post(keystone_client.BASE_URL + 'v2.0/tokens',
json=token)
self.requests.get(
'http://127.0.0.1:8776',
json=fixture_base.generate_version_output()
)
def tearDown(self):
# For some method like test_image_meta_bad_action we are
# For some methods like test_image_meta_bad_action we are
# testing a SystemExit to be thrown and object self.shell has
# no time to get instantatiated which is OK in this case, so
# no time to get instantiated, which is OK in this case, so
# we make sure the method is there before launching it.
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
@@ -83,6 +98,96 @@ class ShellTest(utils.TestCase):
# NOTE(jdg): we default to detail currently
self.assert_called('GET', '/volumes/detail')
def test_list_filter_tenant_with_all_tenants(self):
self.run_command('list --all-tenants=1 --tenant 123')
self.assert_called('GET',
'/volumes/detail?all_tenants=1&project_id=123')
def test_list_filter_tenant_without_all_tenants(self):
self.run_command('list --tenant 123')
self.assert_called('GET',
'/volumes/detail?all_tenants=1&project_id=123')
def test_metadata_args_with_limiter(self):
self.run_command('create --metadata key1="--test1" 1')
self.assert_called('GET', '/volumes/1234')
expected = {'volume': {'imageRef': None,
'project_id': None,
'status': 'creating',
'size': 1,
'user_id': None,
'availability_zone': None,
'source_replica': None,
'attach_status': 'detached',
'source_volid': None,
'consistencygroup_id': None,
'name': None,
'snapshot_id': None,
'metadata': {'key1': '"--test1"'},
'volume_type': None,
'description': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_metadata_args_limiter_display_name(self):
self.run_command('create --metadata key1="--t1" --name="t" 1')
self.assert_called('GET', '/volumes/1234')
expected = {'volume': {'imageRef': None,
'project_id': None,
'status': 'creating',
'size': 1,
'user_id': None,
'availability_zone': None,
'source_replica': None,
'attach_status': 'detached',
'source_volid': None,
'consistencygroup_id': None,
'name': '"t"',
'snapshot_id': None,
'metadata': {'key1': '"--t1"'},
'volume_type': None,
'description': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_delimit_metadata_args(self):
self.run_command('create --metadata key1="test1" key2="test2" 1')
expected = {'volume': {'imageRef': None,
'project_id': None,
'status': 'creating',
'size': 1,
'user_id': None,
'availability_zone': None,
'source_replica': None,
'attach_status': 'detached',
'source_volid': None,
'consistencygroup_id': None,
'name': None,
'snapshot_id': None,
'metadata': {'key1': '"test1"',
'key2': '"test2"'},
'volume_type': None,
'description': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_delimit_metadata_args_display_name(self):
self.run_command('create --metadata key1="t1" --name="t" 1')
self.assert_called('GET', '/volumes/1234')
expected = {'volume': {'imageRef': None,
'project_id': None,
'status': 'creating',
'size': 1,
'user_id': None,
'availability_zone': None,
'source_replica': None,
'attach_status': 'detached',
'source_volid': None,
'consistencygroup_id': None,
'name': '"t"',
'snapshot_id': None,
'metadata': {'key1': '"t1"'},
'volume_type': None,
'description': None}}
self.assert_called_anytime('POST', '/volumes', expected)
def test_list_filter_status(self):
self.run_command('list --status=available')
self.assert_called('GET', '/volumes/detail?status=available')
@@ -103,9 +208,81 @@ class ShellTest(utils.TestCase):
self.run_command('list --limit=10')
self.assert_called('GET', '/volumes/detail?limit=10')
def test_list_sort(self):
self.run_command('list --sort_key=name --sort_dir=asc')
self.assert_called('GET', '/volumes/detail?sort_dir=asc&sort_key=name')
def test_list_sort_valid(self):
self.run_command('list --sort_key=id --sort_dir=asc')
self.assert_called('GET', '/volumes/detail?sort_dir=asc&sort_key=id')
def test_list_sort_key_name(self):
# Client 'name' key is mapped to 'display_name'
self.run_command('list --sort_key=name')
self.assert_called('GET', '/volumes/detail?sort_key=display_name')
def test_list_sort_name(self):
# Client 'name' key is mapped to 'display_name'
self.run_command('list --sort=name')
self.assert_called('GET', '/volumes/detail?sort=display_name')
def test_list_sort_key_invalid(self):
self.assertRaises(ValueError,
self.run_command,
'list --sort_key=foo --sort_dir=asc')
def test_list_sort_dir_invalid(self):
self.assertRaises(ValueError,
self.run_command,
'list --sort_key=id --sort_dir=foo')
def test_list_mix_sort_args(self):
cmds = ['list --sort name:desc --sort_key=status',
'list --sort name:desc --sort_dir=asc',
'list --sort name:desc --sort_key=status --sort_dir=asc']
for cmd in cmds:
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
def test_list_sort_single_key_only(self):
self.run_command('list --sort=id')
self.assert_called('GET', '/volumes/detail?sort=id')
def test_list_sort_single_key_trailing_colon(self):
self.run_command('list --sort=id:')
self.assert_called('GET', '/volumes/detail?sort=id')
def test_list_sort_single_key_and_dir(self):
self.run_command('list --sort=id:asc')
url = '/volumes/detail?%s' % parse.urlencode([('sort', 'id:asc')])
self.assert_called('GET', url)
def test_list_sort_multiple_keys_only(self):
self.run_command('list --sort=id,status,size')
url = ('/volumes/detail?%s' %
parse.urlencode([('sort', 'id,status,size')]))
self.assert_called('GET', url)
def test_list_sort_multiple_keys_and_dirs(self):
self.run_command('list --sort=id:asc,status,size:desc')
url = ('/volumes/detail?%s' %
parse.urlencode([('sort', 'id:asc,status,size:desc')]))
self.assert_called('GET', url)
def test_list_reorder_with_sort(self):
# sortby_index is None if there is sort information
for cmd in ['list --sort_key=name',
'list --sort_dir=asc',
'list --sort_key=name --sort_dir=asc',
'list --sort=name',
'list --sort=name:asc']:
with mock.patch('cinderclient.utils.print_list') as mock_print:
self.run_command(cmd)
mock_print.assert_called_once_with(
mock.ANY, mock.ANY, sortby_index=None)
def test_list_reorder_without_sort(self):
# sortby_index is 0 without sort information
for cmd in ['list', 'list --all-tenants']:
with mock.patch('cinderclient.utils.print_list') as mock_print:
self.run_command(cmd)
mock_print.assert_called_once_with(
mock.ANY, mock.ANY, sortby_index=0)
def test_list_availability_zone(self):
self.run_command('availability-zone-list')
@@ -256,6 +433,11 @@ class ShellTest(utils.TestCase):
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_attach(self):
self.run_command('reset-state --state in-use 1234')
expected = {'os-reset_status': {'status': 'in-use'}}
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'}}
@@ -298,6 +480,55 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('POST', '/snapshots/5678/action',
body=expected)
def test_type_list(self):
self.run_command('type-list')
self.assert_called_anytime('GET', '/types')
def test_type_list_all(self):
self.run_command('type-list --all')
self.assert_called_anytime('GET', '/types?is_public=None')
def test_type_create(self):
self.run_command('type-create test-type-1')
self.assert_called('POST', '/types')
def test_type_create_public(self):
expected = {'volume_type': {'name': 'test-type-1',
'description': 'test_type-1-desc',
'os-volume-type-access:is_public': True}}
self.run_command('type-create test-type-1 '
'--description=test_type-1-desc '
'--is-public=True')
self.assert_called('POST', '/types', body=expected)
def test_type_create_private(self):
expected = {'volume_type': {'name': 'test-type-3',
'description': 'test_type-3-desc',
'os-volume-type-access:is_public': False}}
self.run_command('type-create test-type-3 '
'--description=test_type-3-desc '
'--is-public=False')
self.assert_called('POST', '/types', body=expected)
def test_type_access_list(self):
self.run_command('type-access-list --volume-type 3')
self.assert_called('GET', '/types/3/os-volume-type-access')
def test_type_access_add_project(self):
expected = {'addProjectAccess': {'project': '101'}}
self.run_command('type-access-add --volume-type 3 --project-id 101')
self.assert_called_anytime('GET', '/types/3')
self.assert_called('POST', '/types/3/action',
body=expected)
def test_type_access_remove_project(self):
expected = {'removeProjectAccess': {'project': '101'}}
self.run_command('type-access-remove '
'--volume-type 3 --project-id 101')
self.assert_called_anytime('GET', '/types/3')
self.assert_called('POST', '/types/3/action',
body=expected)
def test_encryption_type_list(self):
"""
Test encryption-type-list shell command.
@@ -445,12 +676,12 @@ class ShellTest(utils.TestCase):
self.assert_called('DELETE', '/snapshots/5678')
def test_volume_manage(self):
self.run_command('manage host1 key1=val1 key2=val2 '
self.run_command('manage host1 some_fake_name '
'--name foo --description bar '
'--volume-type baz --availability-zone az '
'--metadata k1=v1 k2=v2')
expected = {'volume': {'host': 'host1',
'ref': {'key1': 'val1', 'key2': 'val2'},
'ref': {'source-name': 'some_fake_name'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
@@ -466,12 +697,12 @@ class ShellTest(utils.TestCase):
If this flag is specified, then the resulting POST should contain
bootable: True.
"""
self.run_command('manage host1 key1=val1 key2=val2 '
self.run_command('manage host1 some_fake_name '
'--name foo --description bar --bootable '
'--volume-type baz --availability-zone az '
'--metadata k1=v1 k2=v2')
expected = {'volume': {'host': 'host1',
'ref': {'key1': 'val1', 'key2': 'val2'},
'ref': {'source-name': 'some_fake_name'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
@@ -487,14 +718,12 @@ class ShellTest(utils.TestCase):
Checks that the --source-name option correctly updates the
ref structure that is passed in the HTTP POST
"""
self.run_command('manage host1 key1=val1 key2=val2 '
'--source-name VolName '
self.run_command('manage host1 VolName '
'--name foo --description bar '
'--volume-type baz --availability-zone az '
'--metadata k1=v1 k2=v2')
expected = {'volume': {'host': 'host1',
'ref': {'source-name': 'VolName',
'key1': 'val1', 'key2': 'val2'},
'ref': {'source-name': 'VolName'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
@@ -510,14 +739,13 @@ class ShellTest(utils.TestCase):
Checks that the --source-id option correctly updates the
ref structure that is passed in the HTTP POST
"""
self.run_command('manage host1 key1=val1 key2=val2 '
'--source-id 1234 '
self.run_command('manage host1 1234 '
'--id-type source-id '
'--name foo --description bar '
'--volume-type baz --availability-zone az '
'--metadata k1=v1 k2=v2')
expected = {'volume': {'host': 'host1',
'ref': {'source-id': '1234',
'key1': 'val1', 'key2': 'val2'},
'ref': {'source-id': '1234'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
@@ -540,3 +768,32 @@ class ShellTest(utils.TestCase):
self.run_command('replication-reenable 1234')
self.assert_called('POST', '/volumes/1234/action',
body={'os-reenable-replica': None})
def test_create_snapshot_from_volume_with_metadata(self):
"""
Tests create snapshot with --metadata parameter.
Checks metadata params are set during create snapshot
when metadata is passed
"""
expected = {'snapshot': {'volume_id': 1234,
'metadata': {'k1': 'v1',
'k2': 'v2'}}}
self.run_command('snapshot-create 1234 --metadata k1=v1 k2=v2')
self.assert_called_anytime('POST', '/snapshots', partial_body=expected)
def test_get_pools(self):
self.run_command('get-pools')
self.assert_called('GET', '/scheduler-stats/get_pools')
def test_get_pools_detail(self):
self.run_command('get-pools --detail')
self.assert_called('GET', '/scheduler-stats/get_pools?detail=True')
def test_list_transfer(self):
self.run_command('transfer-list')
self.assert_called('GET', '/os-volume-transfer/detail')
def test_list_transfer_all_tenants(self):
self.run_command('transfer-list --all-tenants=1')
self.assert_called('GET', '/os-volume-transfer/detail?all_tenants=1')

View File

@@ -13,9 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import snapshots
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.fixture_data import client
from cinderclient.tests.unit.fixture_data import snapshots
class SnapshotActionsTest(utils.FixturedTestCase):

View File

@@ -0,0 +1,42 @@
# 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 cinderclient.v2 import volume_type_access
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
PROJECT_UUID = '11111111-1111-1111-111111111111'
class TypeAccessTest(utils.TestCase):
def test_list(self):
access = cs.volume_type_access.list(volume_type='3')
cs.assert_called('GET', '/types/3/os-volume-type-access')
for a in access:
self.assertTrue(isinstance(a, volume_type_access.VolumeTypeAccess))
def test_add_project_access(self):
cs.volume_type_access.add_project_access('3', PROJECT_UUID)
cs.assert_called('POST', '/types/3/action',
{'addProjectAccess': {'project': PROJECT_UUID}})
def test_remove_project_access(self):
cs.volume_type_access.remove_project_access('3', PROJECT_UUID)
cs.assert_called('POST', '/types/3/action',
{'removeProjectAccess': {'project': PROJECT_UUID}})

View File

@@ -0,0 +1,88 @@
# 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 cinderclient.v2 import volume_types
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
class TypesTest(utils.TestCase):
def test_list_types(self):
tl = cs.volume_types.list()
cs.assert_called('GET', '/types')
for t in tl:
self.assertIsInstance(t, volume_types.VolumeType)
def test_list_types_not_public(self):
cs.volume_types.list(is_public=None)
cs.assert_called('GET', '/types?is_public=None')
def test_create(self):
t = cs.volume_types.create('test-type-3', 'test-type-3-desc')
cs.assert_called('POST', '/types',
{'volume_type': {
'name': 'test-type-3',
'description': 'test-type-3-desc',
'os-volume-type-access:is_public': True
}})
self.assertIsInstance(t, volume_types.VolumeType)
def test_create_non_public(self):
t = cs.volume_types.create('test-type-3', 'test-type-3-desc', False)
cs.assert_called('POST', '/types',
{'volume_type': {
'name': 'test-type-3',
'description': 'test-type-3-desc',
'os-volume-type-access:is_public': False
}})
self.assertIsInstance(t, volume_types.VolumeType)
def test_update(self):
t = cs.volume_types.update('1', 'test_type_1', 'test_desc_1')
cs.assert_called('PUT',
'/types/1',
{'volume_type': {'name': 'test_type_1',
'description': 'test_desc_1'}})
self.assertIsInstance(t, volume_types.VolumeType)
def test_get(self):
t = cs.volume_types.get('1')
cs.assert_called('GET', '/types/1')
self.assertIsInstance(t, volume_types.VolumeType)
def test_default(self):
t = cs.volume_types.default()
cs.assert_called('GET', '/types/default')
self.assertIsInstance(t, volume_types.VolumeType)
def test_set_key(self):
t = cs.volume_types.get(1)
t.set_keys({'k': 'v'})
cs.assert_called('POST',
'/types/1/extra_specs',
{'extra_specs': {'k': 'v'}})
def test_unsset_keys(self):
t = cs.volume_types.get(1)
t.unset_keys(['k'])
cs.assert_called('DELETE', '/types/1/extra_specs/k')
def test_delete(self):
cs.volume_types.delete(1)
cs.assert_called('DELETE', '/types/1')

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()

View File

@@ -14,8 +14,8 @@
# under the License.
from cinderclient.v2.volume_encryption_types import VolumeEncryptionType
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -14,9 +14,9 @@
# 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.tests.unit import utils
from cinderclient.tests.unit.v2 import fakes
from cinderclient.v2.volumes import Volume
cs = fakes.FakeClient()
@@ -39,6 +39,33 @@ class VolumesTest(utils.TestCase):
self.assertRaises(ValueError,
cs.volumes.list, sort_key='id', sort_dir='invalid')
def test__list(self):
# There only 2 volumes available for our tests, so we set limit to 2.
limit = 2
url = "/volumes?limit=%s" % limit
response_key = "volumes"
fake_volume1234 = Volume(self, {'id': 1234,
'name': 'sample-volume'},
loaded=True)
fake_volume5678 = Volume(self, {'id': 5678,
'name': 'sample-volume2'},
loaded=True)
fake_volumes = [fake_volume1234, fake_volume5678]
# osapi_max_limit is 1000 by default. If limit is less than
# osapi_max_limit, we can get 2 volumes back.
volumes = cs.volumes._list(url, response_key, limit=limit)
cs.assert_called('GET', url)
self.assertEqual(fake_volumes, volumes)
# When we change the osapi_max_limit to 1, the next link should be
# generated. If limit equals 2 and id passed as an argument, we can
# still get 2 volumes back, because the method _list will fetch the
# volume from the next link.
cs.client.osapi_max_limit = 1
volumes = cs.volumes._list(url, response_key, limit=limit)
self.assertEqual(fake_volumes, volumes)
cs.client.osapi_max_limit = 1000
def test_delete_volume(self):
v = cs.volumes.list()[0]
v.delete()
@@ -176,3 +203,77 @@ class VolumesTest(utils.TestCase):
v = cs.volumes.get('1234')
cs.volumes.unmanage(v)
cs.assert_called('POST', '/volumes/1234/action', {'os-unmanage': None})
def test_replication_promote(self):
v = cs.volumes.get('1234')
cs.volumes.promote(v)
cs.assert_called('POST', '/volumes/1234/action',
{'os-promote-replica': None})
def test_replication_reenable(self):
v = cs.volumes.get('1234')
cs.volumes.reenable(v)
cs.assert_called('POST', '/volumes/1234/action',
{'os-reenable-replica': None})
def test_get_pools(self):
cs.volumes.get_pools('')
cs.assert_called('GET', '/scheduler-stats/get_pools')
def test_get_pools_detail(self):
cs.volumes.get_pools('--detail')
cs.assert_called('GET', '/scheduler-stats/get_pools?detail=True')
class FormatSortParamTestCase(utils.TestCase):
def test_format_sort_empty_input(self):
for s in [None, '', []]:
self.assertEqual(None, cs.volumes._format_sort_param(s))
def test_format_sort_string_single_key(self):
s = 'id'
self.assertEqual('id', cs.volumes._format_sort_param(s))
def test_format_sort_string_single_key_and_dir(self):
s = 'id:asc'
self.assertEqual('id:asc', cs.volumes._format_sort_param(s))
def test_format_sort_string_multiple(self):
s = 'id:asc,status,size:desc'
self.assertEqual('id:asc,status,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_string_mappings(self):
s = 'id:asc,name,size:desc'
self.assertEqual('id:asc,display_name,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_whitespace_trailing_comma(self):
s = ' id : asc ,status, size:desc,'
self.assertEqual('id:asc,status,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_list_of_strings(self):
s = ['id:asc', 'status', 'size:desc']
self.assertEqual('id:asc,status,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_list_of_tuples(self):
s = [('id', 'asc'), 'status', ('size', 'desc')]
self.assertEqual('id:asc,status,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_list_of_strings_and_tuples(self):
s = [('id', 'asc'), 'status', 'size:desc']
self.assertEqual('id:asc,status,size:desc',
cs.volumes._format_sort_param(s))
def test_format_sort_invalid_direction(self):
for s in ['id:foo',
'id:asc,status,size:foo',
['id', 'status', 'size:foo'],
['id', 'status', ('size', 'foo')]]:
self.assertRaises(ValueError,
cs.volumes._format_sort_param,
s)

View File

@@ -1,50 +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 cinderclient.v2 import volume_types
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class TypesTest(utils.TestCase):
def test_list_types(self):
tl = cs.volume_types.list()
cs.assert_called('GET', '/types')
for t in tl:
self.assertIsInstance(t, volume_types.VolumeType)
def test_create(self):
t = cs.volume_types.create('test-type-3')
cs.assert_called('POST', '/types')
self.assertIsInstance(t, volume_types.VolumeType)
def test_set_key(self):
t = cs.volume_types.get(1)
t.set_keys({'k': 'v'})
cs.assert_called('POST',
'/types/1/extra_specs',
{'extra_specs': {'k': 'v'}})
def test_unsset_keys(self):
t = cs.volume_types.get(1)
t.unset_keys(['k'])
cs.assert_called('DELETE', '/types/1/extra_specs/k')
def test_delete(self):
cs.volume_types.delete(1)
cs.assert_called('DELETE', '/types/1')

View File

@@ -17,7 +17,6 @@ from __future__ import print_function
import os
import pkg_resources
import re
import sys
import uuid
@@ -62,45 +61,6 @@ def add_arg(f, *args, **kwargs):
f.arguments.insert(0, (args, kwargs))
def add_resource_manager_extra_kwargs_hook(f, hook):
"""Adds hook to bind CLI arguments to ResourceManager calls.
The `do_foo` calls in shell.py will receive CLI args and then in turn pass
them through to the ResourceManager. Before passing through the args, the
hooks registered here will be called, giving us a chance to add extra
kwargs (taken from the command-line) to what's passed to the
ResourceManager.
"""
if not hasattr(f, 'resource_manager_kwargs_hooks'):
f.resource_manager_kwargs_hooks = []
names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
if hook.__name__ not in names:
f.resource_manager_kwargs_hooks.append(hook)
def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
"""Return extra_kwargs by calling resource manager kwargs hooks."""
hooks = getattr(f, "resource_manager_kwargs_hooks", [])
extra_kwargs = {}
for hook in hooks:
hook_name = hook.__name__
hook_kwargs = hook(args)
conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
if conflicting_keys and not allow_conflicts:
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)
return extra_kwargs
def unauthenticated(f):
"""
Adds 'unauthenticated' attribute to decorated function.
@@ -143,10 +103,6 @@ def get_service_type(f):
return getattr(f, 'service_type', None)
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def _print(pt, order):
if sys.version_info >= (3, 0):
print(pt.get_string(sortby=order))
@@ -154,7 +110,17 @@ def _print(pt, order):
print(strutils.safe_encode(pt.get_string(sortby=order)))
def print_list(objs, fields, formatters={}, order_by=None):
def print_list(objs, fields, formatters=None, sortby_index=0):
'''Prints a list of objects.
@param objs: Objects to print
@param fields: Fields on each object to be printed
@param formatters: Custom field formatters
@param sortby_index: Results sorted against the key in the fields list at
this index; if None then the object order is not
altered
'''
formatters = formatters or {}
mixed_case_fields = ['serverId']
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
@@ -173,11 +139,15 @@ def print_list(objs, fields, formatters={}, order_by=None):
data = o[field]
else:
data = getattr(o, field_name, '')
if data is None:
data = '-'
row.append(data)
pt.add_row(row)
if order_by is None:
order_by = fields[0]
if sortby_index is None:
order_by = None
else:
order_by = fields[sortby_index]
_print(pt, order_by)
@@ -281,13 +251,6 @@ def safe_issubclass(*args):
return False
def import_class(import_str):
"""Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
def _load_entry_point(ep_name, name=None):
"""Try to load the entry point ep_name that matches name."""
for ep in pkg_resources.iter_entry_points(ep_name, name=name):
@@ -295,23 +258,3 @@ def _load_entry_point(ep_name, name=None):
return ep.load()
except (ImportError, pkg_resources.UnknownExtra, AttributeError):
continue
_slugify_strip_re = re.compile(r'[^\w\s-]')
_slugify_hyphenate_re = re.compile(r'[-\s]+')
# http://code.activestate.com/recipes/
# 577257-slugify-make-a-string-usable-in-a-url-or-filename/
def slugify(value):
"""
Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens.
From Django's "django/template/defaultfilters.py".
"""
import unicodedata
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())
return _slugify_hyphenate_re.sub('-', value)

View File

@@ -49,7 +49,8 @@ class Client(object):
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='volume', service_name=None,
volume_service_name=None, retries=None, http_log_debug=False,
volume_service_name=None, bypass_url=None,
retries=None, http_log_debug=False,
cacert=None, auth_system='keystone', auth_plugin=None,
session=None, **kwargs):
# FIXME(comstud): Rename the api_key argument above when we
@@ -95,6 +96,7 @@ class Client(object):
service_type=service_type,
service_name=service_name,
volume_service_name=volume_service_name,
bypass_url=bypass_url,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert,

View File

@@ -160,15 +160,24 @@ def _extract_metadata(args):
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='Filters list by metadata key and value pair. '
'Default=None.',
default=None)
'Default=None.')
@utils.arg(
'--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.service_type('volume')
def do_list(cs, args):
"""Lists all volumes."""
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'display_name': args.display_name,
'status': args.status,
'metadata': _extract_metadata(args) if args.metadata else None,
@@ -206,7 +215,7 @@ def do_show(cs, args):
metavar='<snapshot-id>',
default=None,
help='Creates volume from snapshot ID. '
'Default=None.')
'Default=None.')
@utils.arg(
'--snapshot_id',
help=argparse.SUPPRESS)
@@ -215,7 +224,7 @@ def do_show(cs, args):
metavar='<source-volid>',
default=None,
help='Creates volume from volume ID. '
'Default=None.')
'Default=None.')
@utils.arg(
'--source_volid',
help=argparse.SUPPRESS)
@@ -224,7 +233,7 @@ def do_show(cs, args):
metavar='<image-id>',
default=None,
help='Creates volume from image ID. '
'Default=None.')
'Default=None.')
@utils.arg(
'--image_id',
help=argparse.SUPPRESS)
@@ -233,7 +242,7 @@ def do_show(cs, args):
metavar='<display-name>',
default=None,
help='Volume name. '
'Default=None.')
'Default=None.')
@utils.arg(
'--display_name',
help=argparse.SUPPRESS)
@@ -242,7 +251,7 @@ def do_show(cs, args):
metavar='<display-description>',
default=None,
help='Volume description. '
'Default=None.')
'Default=None.')
@utils.arg(
'--display_description',
help=argparse.SUPPRESS)
@@ -251,7 +260,7 @@ def do_show(cs, args):
metavar='<volume-type>',
default=None,
help='Volume type. '
'Default=None.')
'Default=None.')
@utils.arg(
'--volume_type',
help=argparse.SUPPRESS)
@@ -260,7 +269,7 @@ def do_show(cs, args):
metavar='<availability-zone>',
default=None,
help='Availability zone for volume. '
'Default=None.')
'Default=None.')
@utils.arg(
'--availability_zone',
help=argparse.SUPPRESS)
@@ -268,9 +277,9 @@ def do_show(cs, args):
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='Metadata key and value pairs. '
'Default=None.',
default=None)
'Default=None.')
@utils.service_type('volume')
def do_create(cs, args):
"""Creates a volume."""
@@ -293,7 +302,7 @@ def do_create(cs, args):
@utils.arg('volume', metavar='<volume>', nargs='+',
help='Name or ID of volume to delete. '
'Separate multiple volumes with a space.')
'Separate multiple volumes with a space.')
@utils.service_type('volume')
def do_delete(cs, args):
"""Removes one or more volumes."""
@@ -311,7 +320,7 @@ def do_delete(cs, args):
@utils.arg('volume', metavar='<volume>', nargs='+',
help='Name or ID of volume to delete. '
'Separate multiple volumes with a space.')
'Separate multiple volumes with a space.')
@utils.service_type('volume')
def do_force_delete(cs, args):
"""Attempts force-delete of volume, regardless of state."""
@@ -329,12 +338,14 @@ def do_force_delete(cs, args):
@utils.arg('volume', metavar='<volume>', nargs='+',
help='Name or ID of volume to modify. '
'Separate multiple volumes with a space.')
'Separate multiple volumes with a space.')
@utils.arg('--state', metavar='<state>', default='available',
help=('The state to assign to the volume. Valid values are '
'"available," "error," "creating," "deleting," or '
'"error_deleting." '
'Default is "available."'))
'"available," "error," "creating," "deleting," "in-use," '
'"attaching," "detaching" and "error_deleting." '
'NOTE: This command simply changes the state of the '
'Volume in the DataBase with no regard to actual status, '
'exercise caution when using. Default=available.'))
@utils.service_type('volume')
def do_reset_state(cs, args):
"""Explicitly updates the volume state."""
@@ -358,8 +369,7 @@ def do_reset_state(cs, args):
@utils.arg('display_name', nargs='?', metavar='<display-name>',
help='New display name for volume.')
@utils.arg('--display-description', metavar='<display-description>',
help='Volume description. Default=None.',
default=None)
default=None, help='Volume description. Default=None.')
@utils.service_type('volume')
def do_rename(cs, args):
"""Renames a volume."""
@@ -382,14 +392,14 @@ def do_rename(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='The metadata key and pair to set or unset. '
'For unset, specify only the key. '
'Default=[].')
'For unset, specify only the key. '
'Default=[].')
@utils.service_type('volume')
def do_metadata(cs, args):
"""Sets or deletes volume metadata."""
@@ -465,13 +475,13 @@ def do_snapshot_show(cs, args):
help='Name or ID of volume to snapshot.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Allows or disallows snapshot of '
'a volume when the volume is attached to an instance. '
'If set to True, ignores the current status of the '
'volume when attempting to snapshot it rather '
'than forcing it to be available. '
'Default=False.',
default=False)
'a volume when the volume is attached to an instance. '
'If set to True, ignores the current status of the '
'volume when attempting to snapshot it rather '
'than forcing it to be available. '
'Default=False.')
@utils.arg(
'--display-name',
metavar='<display-name>',
@@ -522,8 +532,7 @@ def do_snapshot_delete(cs, args):
@utils.arg('display_name', nargs='?', metavar='<display-name>',
help='New display name for snapshot.')
@utils.arg('--display-description', metavar='<display-description>',
help='Snapshot description. Default=None.',
default=None)
default=None, help='Snapshot description. Default=None.')
@utils.service_type('volume')
def do_snapshot_rename(cs, args):
"""Renames a snapshot."""
@@ -542,12 +551,13 @@ def do_snapshot_rename(cs, args):
@utils.arg('snapshot', metavar='<snapshot>', nargs='+',
help='Name or ID of snapshot to modify.')
@utils.arg('--state', metavar='<state>',
default='available',
@utils.arg('--state', metavar='<state>', default='available',
help=('The state to assign to the snapshot. Valid values are '
'"available," "error," "creating," "deleting," or '
'"error_deleting." '
'Default is "available."'))
'"available," "error," "creating," "deleting," and '
'"error_deleting." NOTE: This command simply changes '
'the state of the Snapshot in the DataBase with no regard '
'to actual status, exercise caution when using. '
'Default=available.'))
@utils.service_type('volume')
def do_snapshot_reset_state(cs, args):
"""Explicitly updates the snapshot state."""
@@ -566,7 +576,7 @@ def do_snapshot_reset_state(cs, args):
if failure_count == len(args.snapshot):
if not single:
msg = ("Unable to reset the state for any of the the specified "
msg = ("Unable to reset the state for any of the specified "
"snapshots.")
raise exceptions.CommandError(msg)
@@ -591,7 +601,7 @@ def do_extra_specs_list(cs, args):
@utils.arg('name',
metavar='<name>',
help="Name for the volume type.")
help='Name for the volume type.')
@utils.service_type('volume')
def do_type_create(cs, args):
"""Creates a volume type."""
@@ -601,7 +611,7 @@ def do_type_create(cs, args):
@utils.arg('id',
metavar='<id>',
help="ID of volume type to delete.")
help='ID of volume type to delete.')
@utils.service_type('volume')
def do_type_delete(cs, args):
"""Deletes a specified volume type."""
@@ -611,17 +621,17 @@ def do_type_delete(cs, args):
@utils.arg('vtype',
metavar='<vtype>',
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='*',
default=None,
help='The extra specs key and value pair to set or unset. '
'For unset, specify only the key. Default=None.')
'For unset, specify only the key. Default=None.')
@utils.service_type('volume')
def do_type_key(cs, args):
"""Sets or unsets extra_spec for a volume type."""
@@ -650,7 +660,8 @@ def do_credentials(cs, args):
utils.print_dict(catalog['token'], "Token")
_quota_resources = ['volumes', 'snapshots', 'gigabytes']
_quota_resources = ['volumes', 'snapshots', 'gigabytes',
'backups', 'backup_gigabytes']
_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit']
@@ -737,6 +748,14 @@ def do_quota_defaults(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='The new "gigabytes" quota value. Default=None.')
@utils.arg('--backups',
metavar='<backups>',
type=int, default=None,
help='The new "backups" quota value. Default=None.')
@utils.arg('--backup-gigabytes',
metavar='<backup_gigabytes>',
type=int, default=None,
help='The new "backup_gigabytes" quota value. Default=None.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
@@ -817,20 +836,20 @@ def _find_volume_type(cs, vtype):
help='Name or ID of volume to upload to an image.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Enables or disables upload of '
'a volume that is attached to an instance. '
'Default=False.',
default=False)
'a volume that is attached to an instance. '
'Default=False.')
@utils.arg('--container-format',
metavar='<container-format>',
default='bare',
help='Container format type. '
'Default is bare.',
default='bare')
'Default is bare.')
@utils.arg('--disk-format',
metavar='<disk-format>',
default='raw',
help='Disk format type. '
'Default is raw.',
default='raw')
'Default is raw.')
@utils.arg('image_name',
metavar='<image-name>',
help='The new image name.')
@@ -847,14 +866,14 @@ def do_upload_to_image(cs, args):
@utils.arg('volume', metavar='<volume>',
help='Name or ID of volume to back up.')
@utils.arg('--container', metavar='<container>',
help='Backup container name. Default=None.',
default=None)
default=None,
help='Backup container name. Default=None.')
@utils.arg('--display-name', metavar='<display-name>',
help='Backup name. Default=None.',
default=None)
default=None,
help='Backup name. Default=None.')
@utils.arg('--display-description', metavar='<display-description>',
help='Backup description. Default=None.',
default=None)
default=None,
help='Backup description. Default=None.')
@utils.service_type('volume')
def do_backup_create(cs, args):
"""Creates a volume backup."""
@@ -908,9 +927,9 @@ def do_backup_delete(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of backup to restore.')
@utils.arg('--volume-id', metavar='<volume>',
default=None,
help='ID or name of backup volume to '
'which to restore. Default=None.',
default=None)
'which to restore. Default=None.')
@utils.service_type('volume')
def do_backup_restore(cs, args):
"""Restores a backup."""
@@ -924,8 +943,8 @@ def do_backup_restore(cs, args):
@utils.arg('volume', metavar='<volume>',
help='Name or ID of volume to transfer.')
@utils.arg('--display-name', metavar='<display-name>',
help='Transfer name. Default=None.',
default=None)
default=None,
help='Transfer name. Default=None.')
@utils.service_type('volume')
def do_transfer_create(cs, args):
"""Creates a volume transfer."""
@@ -967,10 +986,29 @@ def do_transfer_accept(cs, args):
utils.print_dict(info)
@utils.arg(
'--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg(
'--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.service_type('volume')
def do_transfer_list(cs, args):
"""Lists all transfers."""
transfers = cs.transfers.list()
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
}
transfers = cs.transfers.list(search_opts=search_opts)
columns = ['ID', 'Volume ID', 'Name']
utils.print_list(transfers, columns)
@@ -1128,7 +1166,7 @@ def do_encryption_type_list(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.service_type('volume')
def do_encryption_type_show(cs, args):
"""Shows encryption type details for volume type. Admin only."""
@@ -1146,26 +1184,26 @@ def do_encryption_type_show(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.arg('provider',
metavar='<provider>',
type=str,
help='The class that provides encryption support. '
'For example, a volume driver class path.')
'For example, a volume driver class path.')
@utils.arg('--cipher',
metavar='<cipher>',
type=str,
required=False,
default=None,
help='The encryption algorithm and mode. '
'For example, aes-xts-plain64. Default=None.')
'For example, aes-xts-plain64. Default=None.')
@utils.arg('--key_size',
metavar='<key_size>',
type=int,
required=False,
default=None,
help='Size of encryption key, in bits. '
'For example, 128 or 256. Default=None.')
'For example, 128 or 256. Default=None.')
@utils.arg('--control_location',
metavar='<control_location>',
choices=['front-end', 'back-end'],
@@ -1173,9 +1211,9 @@ def do_encryption_type_show(cs, args):
required=False,
default='front-end',
help='Notional service where encryption is performed. '
'Valid values are "front-end" or "back-end." '
'For example, front-end=Nova. '
'Default is "front-end."')
'Valid values are "front-end" or "back-end." '
'For example, front-end=Nova. '
'Default is "front-end."')
@utils.service_type('volume')
def do_encryption_type_create(cs, args):
"""Creates encryption type for a volume type. Admin only."""
@@ -1194,7 +1232,7 @@ def do_encryption_type_create(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.service_type('volume')
def do_encryption_type_delete(cs, args):
"""Deletes encryption type for a volume type. Admin only."""
@@ -1206,10 +1244,10 @@ def do_encryption_type_delete(cs, args):
@utils.arg('host', metavar='<host>', help='Destination host.')
@utils.arg('--force-host-copy', metavar='<True|False>',
choices=['True', 'False'], required=False,
default=False,
help='Enables or disables generic host-based '
'force-migration, which bypasses driver '
'optimizations. Default=False.',
default=False)
'force-migration, which bypasses driver '
'optimizations. Default=False.')
@utils.service_type('volume')
def do_migrate(cs, args):
"""Migrates volume to a new host."""
@@ -1236,7 +1274,7 @@ def _print_associations_list(associations):
@utils.arg('name',
metavar='<name>',
help="Name of new QoS specifications.")
help='Name of new QoS specifications.')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
@@ -1316,12 +1354,12 @@ def do_qos_disassociate_all(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata', metavar='key=value',
nargs='+',
default=[],
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
def do_qos_key(cs, args):
"""Sets or unsets specifications for a qos spec."""
keypair = _extract_metadata(args)
@@ -1347,13 +1385,13 @@ def do_qos_get_association(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='The metadata key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
@utils.service_type('volume')
def do_snapshot_metadata(cs, args):
"""Sets or deletes snapshot metadata."""
@@ -1393,14 +1431,14 @@ def do_metadata_show(cs, args):
nargs='+',
default=[],
help='Metadata key and value pair or pairs to update. '
'Default=[].')
'Default=[].')
@utils.service_type('volume')
def do_metadata_update_all(cs, args):
"""Updates volume metadata."""
volume = utils.find_volume(cs, args.volume)
metadata = _extract_metadata(args)
metadata = volume.update_all_metadata(metadata)
utils.print_dict(metadata)
utils.print_dict(metadata['metadata'], 'Metadata-property')
@utils.arg('snapshot',
@@ -1411,7 +1449,7 @@ def do_metadata_update_all(cs, args):
nargs='+',
default=[],
help='Metadata key and value pair or pairs to update. '
'Default=[].')
'Default=[].')
@utils.service_type('volume')
def do_snapshot_metadata_update_all(cs, args):
"""Updates snapshot metadata."""
@@ -1426,7 +1464,7 @@ def do_snapshot_metadata_update_all(cs, args):
metavar='<True|true|False|false>',
choices=['True', 'true', 'False', 'false'],
help='Enables or disables update of volume to '
'read-only access mode.')
'read-only access mode.')
@utils.service_type('volume')
def do_readonly_mode_update(cs, args):
"""Updates volume read-only access-mode flag."""

View File

@@ -58,7 +58,7 @@ class VolumeBackupManager(base.ManagerWithFind):
"""
return self._get("/backups/%s" % backup_id, "backup")
def list(self, detailed=True):
def list(self, detailed=True, search_opts=None):
"""Get a list of all volume backups.
:rtype: list of :class:`VolumeBackup`

View File

@@ -17,6 +17,11 @@
Volume transfer interface (1.1 extension).
"""
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import six
from cinderclient import base
@@ -69,10 +74,23 @@ class VolumeTransferManager(base.ManagerWithFind):
: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")
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/os-volume-transfer%s%s" % (detail, query_string),
"transfers")
def delete(self, transfer_id):
"""Delete a volume transfer.

View File

@@ -118,11 +118,6 @@ 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 update_all_metadata(self, metadata):
"""Update all metadata of this volume."""
return self.manager.update_all_metadata(self, metadata)
@@ -210,7 +205,13 @@ class VolumeManager(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:

View File

@@ -18,6 +18,7 @@ from cinderclient.v2 import availability_zones
from cinderclient.v2 import cgsnapshots
from cinderclient.v2 import consistencygroups
from cinderclient.v2 import limits
from cinderclient.v2 import pools
from cinderclient.v2 import qos_specs
from cinderclient.v2 import quota_classes
from cinderclient.v2 import quotas
@@ -25,10 +26,11 @@ 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_type_access
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
from cinderclient.v2 import volume_transfers
class Client(object):
@@ -49,9 +51,9 @@ class Client(object):
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='volumev2', service_name=None,
volume_service_name=None, retries=None, http_log_debug=False,
cacert=None, auth_system='keystone', auth_plugin=None,
session=None, **kwargs):
volume_service_name=None, bypass_url=None, retries=None,
http_log_debug=False, cacert=None, auth_system='keystone',
auth_plugin=None, session=None, **kwargs):
# FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument
password = api_key
@@ -61,6 +63,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_type_access = \
volume_type_access.VolumeTypeAccessManager(self)
self.volume_encryption_types = \
volume_encryption_types.VolumeEncryptionTypeManager(self)
self.qos_specs = qos_specs.QoSSpecsManager(self)
@@ -75,6 +79,7 @@ class Client(object):
self.cgsnapshots = cgsnapshots.CgsnapshotManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
self.pools = pools.PoolManager(self)
# Add in any extensions...
if extensions:
@@ -98,6 +103,7 @@ class Client(object):
service_type=service_type,
service_name=service_name,
volume_service_name=volume_service_name,
bypass_url=bypass_url,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert,

62
cinderclient/v2/pools.py Normal file
View File

@@ -0,0 +1,62 @@
# Copyright (C) 2015 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.
"""Pools interface (v2 extension)"""
import six
from cinderclient import base
class Pool(base.Resource):
NAME_ATTR = 'name'
def __repr__(self):
return "<Pool: %s>" % self.name
class PoolManager(base.Manager):
"""Manage :class:`Pool` resources."""
resource_class = Pool
def list(self, detailed=False):
"""Lists all
:rtype: list of :class:`Pool`
"""
if detailed is True:
pools = self._list("/scheduler-stats/get_pools?detail=True",
"pools")
# Other than the name, all of the pool data is buried below in
# a 'capabilities' dictionary. In order to be consistent with the
# get-pools command line, these elements are moved up a level to
# be attributes of the pool itself.
for pool in pools:
if hasattr(pool, 'capabilities'):
for k, v in six.iteritems(pool.capabilities):
setattr(pool, k, v)
# Remove the capabilities dictionary since all of its
# elements have been copied up to the containing pool
del pool.capabilities
return pools
else:
pools = self._list("/scheduler-stats/get_pools", "pools")
# avoid cluttering the basic pool list with capabilities dict
for pool in pools:
if hasattr(pool, 'capabilities'):
del pool.capabilities
return pools

View File

@@ -28,6 +28,7 @@ from cinderclient import exceptions
from cinderclient import utils
from cinderclient.openstack.common import strutils
from cinderclient.v2 import availability_zones
from cinderclient.v2 import volumes
def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
@@ -163,15 +164,15 @@ def _extract_metadata(args):
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='Filters results by a metadata key and value pair. '
'OPTIONAL: Default=None.',
default=None)
'OPTIONAL: Default=None.')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'OPTIONAL: Default=None.')
'list than that represented by this volume id. '
'OPTIONAL: Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
@@ -179,14 +180,24 @@ def _extract_metadata(args):
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help='Key to be sorted, should be (`id`, `status`, `size`, '
'`availability_zone`, `name`, `bootable`, `created_at`). '
'OPTIONAL: Default=None.')
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help='Sort direction, should be `desc` or `asc`. '
'OPTIONAL: Default=None.')
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. OPTIONAL: '
'Default=None.') % ', '.join(volumes.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.service_type('volumev2')
def do_list(cs, args):
"""Lists all volumes."""
@@ -194,16 +205,26 @@ def do_list(cs, args):
if args.display_name is not None:
args.name = args.display_name
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'status': args.status,
'metadata': _extract_metadata(args) if args.metadata else None,
}
# --sort_key and --sort_dir deprecated in kilo and is not supported
# with --sort
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir)
sort_dir=args.sort_dir, sort=args.sort)
_translate_volume_keys(volumes)
# Create a list of servers to which the volume is attached
@@ -217,7 +238,11 @@ def do_list(cs, args):
else:
key_list = ['ID', 'Status', 'Name',
'Size', 'Volume Type', 'Bootable', 'Attached to']
utils.print_list(volumes, key_list)
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(volumes, key_list, sortby_index=sortby_index)
@utils.arg('volume',
@@ -277,6 +302,12 @@ class CheckSizeArgForCreate(argparse.Action):
help='Creates volume from image ID. Default=None.')
@utils.arg('--image_id',
help=argparse.SUPPRESS)
@utils.arg('--image',
metavar='<image>',
default=None,
help='Creates a volume from image (ID or name). Default=None.')
@utils.arg('--image_ref',
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
@@ -309,8 +340,8 @@ class CheckSizeArgForCreate(argparse.Action):
type=str,
nargs='*',
metavar='<key=value>',
help='Metadata key and value pairs. Default=None.',
default=None)
default=None,
help='Metadata key and value pairs. Default=None.')
@utils.arg('--hint',
metavar='<key=value>',
dest='scheduler_hints',
@@ -331,7 +362,7 @@ def do_create(cs, args):
if args.metadata is not None:
volume_metadata = _extract_metadata(args)
#NOTE(N.S.): take this piece from novaclient
# NOTE(N.S.): take this piece from novaclient
hints = {}
if args.scheduler_hints:
for hint in args.scheduler_hints:
@@ -344,7 +375,10 @@ def do_create(cs, args):
hints[key] += [value]
else:
hints[key] = value
#NOTE(N.S.): end of taken piece
# NOTE(N.S.): end of taken piece
# Keep backward compatibility with image_id, favoring explicit ID
image_ref = args.image_id or args.image_ref
volume = cs.volumes.create(args.size,
args.consisgroup_id,
@@ -354,7 +388,7 @@ def do_create(cs, args):
args.description,
args.volume_type,
availability_zone=args.availability_zone,
imageRef=args.image_id,
imageRef=image_ref,
metadata=volume_metadata,
scheduler_hints=hints,
source_replica=args.source_replica)
@@ -381,7 +415,7 @@ def do_delete(cs, args):
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 specified "
raise exceptions.CommandError("Unable to delete any of the specified "
"volumes.")
@@ -399,7 +433,7 @@ def do_force_delete(cs, args):
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 "
raise exceptions.CommandError("Unable to force delete any of the "
"specified volumes.")
@@ -407,12 +441,21 @@ def do_force_delete(cs, args):
help='Name or ID of volume to modify.')
@utils.arg('--state', metavar='<state>', default='available',
help=('The state to assign to the volume. Valid values are '
'"available," "error," "creating," "deleting," and '
'"error_deleting." '
'Default=available.'))
'"available," "error," "creating," "deleting," "in-use," '
'"attaching," "detaching" and "error_deleting." '
'NOTE: This command simply changes the state of the '
'Volume in the DataBase with no regard to actual status, '
'exercise caution when using. Default=available.'))
@utils.service_type('volumev2')
def do_reset_state(cs, args):
"""Explicitly updates the volume state."""
"""Explicitly updates the volume state in the Cinder database.
Note that this does not affect whether the volume is actually attached to
the Nova compute host or instance and can result in an unusable volume.
Being a database change only, this has no impact on the true state of the
volume and may not match the actual state. This can render a volume
unusable in the case of change to the 'available' state.
"""
failure_flag = False
for volume in args.volume:
@@ -467,13 +510,13 @@ def do_rename(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
@utils.service_type('volumev2')
def do_metadata(cs, args):
"""Sets or deletes volume metadata."""
@@ -555,13 +598,13 @@ def do_snapshot_show(cs, args):
help='Name or ID of volume to snapshot.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Allows or disallows snapshot of '
'a volume when the volume is attached to an instance. '
'If set to True, ignores the current status of the '
'volume when attempting to snapshot it rather '
'than forcing it to be available. '
'Default=False.',
default=False)
'a volume when the volume is attached to an instance. '
'If set to True, ignores the current status of the '
'volume when attempting to snapshot it rather '
'than forcing it to be available. '
'Default=False.')
@utils.arg('--name',
metavar='<name>',
default=None,
@@ -578,6 +621,12 @@ def do_snapshot_show(cs, args):
help=argparse.SUPPRESS)
@utils.arg('--display_description',
help=argparse.SUPPRESS)
@utils.arg('--metadata',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='Snapshot metadata key and value pairs. Default=None.')
@utils.service_type('volumev2')
def do_snapshot_create(cs, args):
"""Creates a snapshot."""
@@ -587,11 +636,16 @@ def do_snapshot_create(cs, args):
if args.display_description is not None:
args.description = args.display_description
snapshot_metadata = None
if args.metadata is not None:
snapshot_metadata = _extract_metadata(args)
volume = utils.find_volume(cs, args.volume)
snapshot = cs.volume_snapshots.create(volume.id,
args.force,
args.name,
args.description)
args.description,
metadata=snapshot_metadata)
_print_volume_snapshot(snapshot)
@@ -618,8 +672,8 @@ def do_snapshot_delete(cs, args):
@utils.arg('name', nargs='?', metavar='<name>',
help='New name for snapshot.')
@utils.arg('--description', metavar='<description>',
help='Snapshot description. Default=None.',
default=None)
default=None,
help='Snapshot description. Default=None.')
@utils.arg('--display-description',
help=argparse.SUPPRESS)
@utils.arg('--display_description',
@@ -649,9 +703,11 @@ def do_snapshot_rename(cs, args):
@utils.arg('--state', metavar='<state>',
default='available',
help=('The state to assign to the snapshot. Valid values are '
'"available," "error," "creating," "deleting," and '
'"error_deleting." '
'Default is "available."'))
'"available," "error," "creating," "deleting," and '
'"error_deleting." NOTE: This command simply changes '
'the state of the Snapshot in the DataBase with no regard '
'to actual status, exercise caution when using. '
'Default=available.'))
@utils.service_type('volumev2')
def do_snapshot_reset_state(cs, args):
"""Explicitly updates the snapshot state."""
@@ -676,16 +732,47 @@ def do_snapshot_reset_state(cs, args):
def _print_volume_type_list(vtypes):
utils.print_list(vtypes, ['ID', 'Name'])
utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public'])
@utils.service_type('volumev2')
@utils.arg('--all',
dest='all',
action='store_true',
default=False,
help='Display all volume types (Admin only).')
def do_type_list(cs, args):
"""Lists available 'volume types'."""
vtypes = cs.volume_types.list()
if args.all:
vtypes = cs.volume_types.list(is_public=None)
else:
vtypes = cs.volume_types.list()
_print_volume_type_list(vtypes)
@utils.service_type('volumev2')
def do_type_default(cs, args):
"""List the default volume type."""
vtype = cs.volume_types.default()
_print_volume_type_list([vtype])
@utils.arg('id',
metavar='<id>',
help='ID of the volume type.')
@utils.arg('--name',
metavar='<name>',
help='Name of the volume type.')
@utils.arg('--description',
metavar='<description>',
help='Description of the volume type.')
@utils.service_type('volumev2')
def do_type_update(cs, args):
"""Updates volume type name and/or description."""
vtype = cs.volume_types.update(args.id, args.name, args.description)
_print_volume_type_list([vtype])
@utils.service_type('volumev2')
def do_extra_specs_list(cs, args):
"""Lists current volume types and extra specs."""
@@ -695,17 +782,25 @@ def do_extra_specs_list(cs, args):
@utils.arg('name',
metavar='<name>',
help="Name of new volume type.")
help='Name of new volume type.')
@utils.arg('--description',
metavar='<description>',
help='Description of new volume type.')
@utils.arg('--is-public',
metavar='<is-public>',
default=True,
help='Make type accessible to the public (default true).')
@utils.service_type('volumev2')
def do_type_create(cs, args):
"""Creates a volume type."""
vtype = cs.volume_types.create(args.name)
is_public = strutils.bool_from_string(args.is_public)
vtype = cs.volume_types.create(args.name, args.description, is_public)
_print_volume_type_list([vtype])
@utils.arg('id',
metavar='<id>',
help="ID of volume type to delete.")
help='ID of volume type to delete.')
@utils.service_type('volumev2')
def do_type_delete(cs, args):
"""Deletes a volume type."""
@@ -714,17 +809,17 @@ def do_type_delete(cs, args):
@utils.arg('vtype',
metavar='<vtype>',
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='The extra specs key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
@utils.service_type('volumev2')
def do_type_key(cs, args):
"""Sets or unsets extra_spec for a volume type."""
@@ -737,6 +832,45 @@ def do_type_key(cs, args):
vtype.unset_keys(list(keypair))
@utils.arg('--volume-type', metavar='<volume_type>', required=True,
help='Filter results by volume type name or ID.')
@utils.service_type('volumev2')
def do_type_access_list(cs, args):
"""Print access information about the given volume type."""
volume_type = _find_volume_type(cs, args.volume_type)
if volume_type.is_public:
raise exceptions.CommandError("Failed to get access list "
"for public volume type.")
access_list = cs.volume_type_access.list(volume_type)
columns = ['Volume_type_ID', 'Project_ID']
utils.print_list(access_list, columns)
@utils.arg('--volume-type', metavar='<volume_type>', required=True,
help='Volume type name or ID to add access for the given project.')
@utils.arg('--project-id', metavar='<project_id>', required=True,
help='Project ID to add volume type access for.')
@utils.service_type('volumev2')
def do_type_access_add(cs, args):
"""Adds volume type access for the given project."""
vtype = _find_volume_type(cs, args.volume_type)
cs.volume_type_access.add_project_access(vtype, args.project_id)
@utils.arg('--volume-type', metavar='<volume_type>', required=True,
help=('Volume type name or ID to remove access '
'for the given project.'))
@utils.arg('--project-id', metavar='<project_id>', required=True,
help='Project ID to remove volume type access for.')
@utils.service_type('volumev2')
def do_type_access_remove(cs, args):
"""Removes volume type access for the given project."""
vtype = _find_volume_type(cs, args.volume_type)
cs.volume_type_access.remove_project_access(
vtype, args.project_id)
@utils.service_type('volumev2')
def do_endpoints(cs, args):
"""Discovers endpoints registered by authentication service."""
@@ -753,7 +887,9 @@ def do_credentials(cs, args):
utils.print_dict(catalog['token'], "Token")
_quota_resources = ['volumes', 'snapshots', 'gigabytes']
_quota_resources = ['volumes', 'snapshots', 'gigabytes',
'backups', 'backup_gigabytes',
'consistencygroups']
_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit']
@@ -843,6 +979,18 @@ def do_quota_defaults(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='The new "gigabytes" quota value. Default=None.')
@utils.arg('--backups',
metavar='<backups>',
type=int, default=None,
help='The new "backups" quota value. Default=None.')
@utils.arg('--backup-gigabytes',
metavar='<backup_gigabytes>',
type=int, default=None,
help='The new "backup_gigabytes" quota value. Default=None.')
@utils.arg('--consistencygroups',
metavar='<consistencygroups>',
type=int, default=None,
help='The new "consistencygroups" quota value. Default=None.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
@@ -925,22 +1073,22 @@ def _find_volume_type(cs, vtype):
help='Name or ID of volume to snapshot.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Enables or disables upload of '
'a volume that is attached to an instance. '
'Default=False.',
default=False)
'a volume that is attached to an instance. '
'Default=False.')
@utils.arg('--container-format',
metavar='<container-format>',
default='bare',
help='Container format type. '
'Default is bare.',
default='bare')
'Default is bare.')
@utils.arg('--container_format',
help=argparse.SUPPRESS)
@utils.arg('--disk-format',
metavar='<disk-format>',
default='raw',
help='Disk format type. '
'Default is raw.',
default='raw')
'Default is raw.')
@utils.arg('--disk_format',
help=argparse.SUPPRESS)
@utils.arg('image_name',
@@ -962,10 +1110,10 @@ def do_upload_to_image(cs, args):
@utils.arg('host', metavar='<host>', help='Destination host.')
@utils.arg('--force-host-copy', metavar='<True|False>',
choices=['True', 'False'], required=False,
default=False,
help='Enables or disables generic host-based '
'force-migration, which bypasses driver '
'optimizations. Default=False.',
default=False)
'force-migration, which bypasses driver '
'optimizations. Default=False.')
@utils.service_type('volumev2')
def do_migrate(cs, args):
"""Migrates volume to a new host."""
@@ -989,13 +1137,13 @@ def do_retype(cs, args):
@utils.arg('volume', metavar='<volume>',
help='Name or ID of volume to backup.')
@utils.arg('--container', metavar='<container>',
help='Backup container name. Default=None.',
default=None)
default=None,
help='Backup container name. Default=None.')
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.arg('--name', metavar='<name>',
help='Backup name. Default=None.',
default=None)
default=None,
help='Backup name. Default=None.')
@utils.arg('--display-description',
help=argparse.SUPPRESS)
@utils.arg('--description',
@@ -1059,12 +1207,12 @@ def do_backup_delete(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of backup to restore.')
@utils.arg('--volume-id', metavar='<volume>',
help=argparse.SUPPRESS,
default=None)
default=None,
help=argparse.SUPPRESS)
@utils.arg('--volume', metavar='<volume>',
default=None,
help='Name or ID of volume to which to restore. '
'Default=None.',
default=None)
'Default=None.')
@utils.service_type('volumev2')
def do_backup_restore(cs, args):
"""Restores a backup."""
@@ -1146,10 +1294,27 @@ def do_transfer_accept(cs, args):
utils.print_dict(info)
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.service_type('volumev2')
def do_transfer_list(cs, args):
"""Lists all transfers."""
transfers = cs.transfers.list()
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
}
transfers = cs.transfers.list(search_opts=search_opts)
columns = ['ID', 'Volume ID', 'Name']
utils.print_list(transfers, columns)
@@ -1305,7 +1470,7 @@ def do_encryption_type_list(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.service_type('volumev2')
def do_encryption_type_show(cs, args):
"""Shows encryption type details for a volume type. Admin only."""
@@ -1323,26 +1488,26 @@ def do_encryption_type_show(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.arg('provider',
metavar='<provider>',
type=str,
help='The class that provides encryption support. '
'For example, LuksEncryptor.')
'For example, LuksEncryptor.')
@utils.arg('--cipher',
metavar='<cipher>',
type=str,
required=False,
default=None,
help='The encryption algorithm or mode. '
'For example, aes-xts-plain64. Default=None.')
'For example, aes-xts-plain64. Default=None.')
@utils.arg('--key_size',
metavar='<key_size>',
type=int,
required=False,
default=None,
help='Size of encryption key, in bits. '
'For example, 128 or 256. Default=None.')
'For example, 128 or 256. Default=None.')
@utils.arg('--control_location',
metavar='<control_location>',
choices=['front-end', 'back-end'],
@@ -1350,8 +1515,8 @@ def do_encryption_type_show(cs, args):
required=False,
default='front-end',
help='Notional service where encryption is performed. '
'Valid values are "front-end" or "back-end." '
'For example, front-end=Nova. Default is "front-end."')
'Valid values are "front-end" or "back-end." '
'For example, front-end=Nova. Default is "front-end."')
@utils.service_type('volumev2')
def do_encryption_type_create(cs, args):
"""Creates encryption type for a volume type. Admin only."""
@@ -1370,7 +1535,7 @@ def do_encryption_type_create(cs, args):
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of volume type.")
help='Name or ID of volume type.')
@utils.service_type('volumev2')
def do_encryption_type_delete(cs, args):
"""Deletes encryption type for a volume type. Admin only."""
@@ -1396,12 +1561,12 @@ def _print_associations_list(associations):
@utils.arg('name',
metavar='<name>',
help="Name of new QoS specifications.")
help='Name of new QoS specifications.')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help="QoS specifications.")
help='QoS specifications.')
@utils.service_type('volumev2')
def do_qos_create(cs, args):
"""Creates a qos specs."""
@@ -1420,7 +1585,7 @@ def do_qos_list(cs, args):
@utils.arg('qos_specs', metavar='<qos_specs>',
help="ID of QoS specifications to show.")
help='ID of QoS specifications to show.')
@utils.service_type('volumev2')
def do_qos_show(cs, args):
"""Shows qos specs details."""
@@ -1429,7 +1594,7 @@ def do_qos_show(cs, args):
@utils.arg('qos_specs', metavar='<qos_specs>',
help="ID of QoS specifications to delete.")
help='ID of QoS specifications to delete.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
@@ -1447,7 +1612,7 @@ def do_qos_delete(cs, args):
help='ID of QoS specifications.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type with which to associate '
'QoS specifications.')
'QoS specifications.')
@utils.service_type('volumev2')
def do_qos_associate(cs, args):
"""Associates qos specs with specified volume type."""
@@ -1458,7 +1623,7 @@ def do_qos_associate(cs, args):
help='ID of QoS specifications.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type with which to associate '
'QoS specifications.')
'QoS specifications.')
@utils.service_type('volumev2')
def do_qos_disassociate(cs, args):
"""Disassociates qos specs from specified volume type."""
@@ -1478,12 +1643,12 @@ def do_qos_disassociate_all(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata', metavar='key=value',
nargs='+',
default=[],
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
def do_qos_key(cs, args):
"""Sets or unsets specifications for a qos spec."""
keypair = _extract_metadata(args)
@@ -1509,13 +1674,13 @@ def do_qos_get_association(cs, args):
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="The action. Valid values are 'set' or 'unset.'")
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key.')
'For unset, specify only the key.')
@utils.service_type('volumev2')
def do_snapshot_metadata(cs, args):
"""Sets or deletes snapshot metadata."""
@@ -1561,7 +1726,7 @@ def do_metadata_update_all(cs, args):
volume = utils.find_volume(cs, args.volume)
metadata = _extract_metadata(args)
metadata = volume.update_all_metadata(metadata)
utils.print_dict(metadata)
utils.print_dict(metadata['metadata'], 'Metadata-property')
@utils.arg('snapshot',
@@ -1586,7 +1751,7 @@ def do_snapshot_metadata_update_all(cs, args):
metavar='<True|true|False|false>',
choices=['True', 'true', 'False', 'false'],
help='Enables or disables update of volume to '
'read-only access mode.')
'read-only access mode.')
@utils.service_type('volumev2')
def do_readonly_mode_update(cs, args):
"""Updates volume read-only access-mode flag."""
@@ -1610,36 +1775,33 @@ def do_set_bootable(cs, args):
@utils.arg('host',
metavar='<host>',
help='Cinder host on which the existing volume resides')
@utils.arg('ref',
type=str,
nargs='*',
metavar='<key=value>',
help='Driver-specific reference to the existing volume as '
'key=value pairs')
@utils.arg('--source-name',
metavar='<source-name>',
help='Name of the volume to manage (Optional)')
@utils.arg('--source-id',
metavar='<source-id>',
help='ID of the volume to manage (Optional)')
help='Cinder host on which the existing volume resides; '
'takes the form: host@backend-name#pool')
@utils.arg('identifier',
metavar='<identifier>',
help='Name or other Identifier for existing volume')
@utils.arg('--id-type',
metavar='<id-type>',
default='source-name',
help='Type of backend device identifier provided, '
'typically source-name or source-id (Default=source-name)')
@utils.arg('--name',
metavar='<name>',
help='Volume name (Optional, Default=None)')
help='Volume name (Default=None)')
@utils.arg('--description',
metavar='<description>',
help='Volume description (Optional, Default=None)')
help='Volume description (Default=None)')
@utils.arg('--volume-type',
metavar='<volume-type>',
help='Volume type (Optional, Default=None)')
help='Volume type (Default=None)')
@utils.arg('--availability-zone',
metavar='<availability-zone>',
help='Availability zone for volume (Optional, Default=None)')
help='Availability zone for volume (Default=None)')
@utils.arg('--metadata',
type=str,
nargs='*',
metavar='<key=value>',
help='Metadata key=value pairs (Optional, Default=None)')
help='Metadata key=value pairs (Default=None)')
@utils.arg('--bootable',
action='store_true',
help='Specifies that the newly created volume should be'
@@ -1653,9 +1815,7 @@ def do_manage(cs, args):
# Build a dictionary of key/value pairs to pass to the API.
ref_dict = {}
for pair in args.ref:
(k, v) = pair.split('=', 1)
ref_dict[k] = v
ref_dict[args.id_type] = args.identifier
# The recommended way to specify an existing volume is by ID or name, and
# have the Cinder driver look for 'source-name' or 'source-id' elements in
@@ -1666,6 +1826,7 @@ def do_manage(cs, args):
# Note how argparse converts hyphens to underscores. We use hyphens in the
# dictionary so that it is consistent with what the user specified on the
# CLI.
if hasattr(args, 'source_name') and \
args.source_name is not None:
ref_dict['source-name'] = args.source_name
@@ -1693,7 +1854,9 @@ def do_manage(cs, args):
help='Name or ID of the volume to unmanage.')
@utils.service_type('volumev2')
def do_unmanage(cs, args):
utils.find_volume(cs, args.volume).unmanage(args.volume)
"""Stop managing a volume."""
volume = utils.find_volume(cs, args.volume)
cs.volumes.unmanage(volume.id)
@utils.arg('volume', metavar='<volume>',
@@ -1701,7 +1864,8 @@ def do_unmanage(cs, args):
@utils.service_type('volumev2')
def do_replication_promote(cs, args):
"""Promote a secondary volume to primary for a relationship."""
utils.find_volume(cs, args.volume).promote(args.volume)
volume = utils.find_volume(cs, args.volume)
cs.volumes.promote(volume.id)
@utils.arg('volume', metavar='<volume>',
@@ -1709,7 +1873,8 @@ def do_replication_promote(cs, args):
@utils.service_type('volumev2')
def do_replication_reenable(cs, args):
"""Sync the secondary volume with primary for a relationship."""
utils.find_volume(cs, args.volume).reenable(args.volume)
volume = utils.find_volume(cs, args.volume)
cs.volumes.reenable(volume.id)
@utils.arg('--all-tenants',
@@ -1781,12 +1946,12 @@ def do_consisgroup_create(cs, args):
'to be deleted.')
@utils.arg('--force',
action='store_true',
default=False,
help='Allows or disallows consistency groups '
'to be deleted. If the consistency group is empty, '
'it can be deleted without the force flag. '
'If the consistency group is not empty, the force '
'flag is required for it to be deleted.',
default=False)
'flag is required for it to be deleted.')
@utils.service_type('volumev2')
def do_consisgroup_delete(cs, args):
"""Removes one or more consistency groups."""
@@ -1799,7 +1964,7 @@ def do_consisgroup_delete(cs, args):
print("Delete for consistency group %s failed: %s" %
(consistencygroup, e))
if failure_count == len(args.consistencygroup):
raise exceptions.CommandError("Unable to delete any of specified "
raise exceptions.CommandError("Unable to delete any of the specified "
"consistency groups.")
@@ -1894,5 +2059,23 @@ def do_cgsnapshot_delete(cs, args):
failure_count += 1
print("Delete for cgsnapshot %s failed: %s" % (cgsnapshot, e))
if failure_count == len(args.cgsnapshot):
raise exceptions.CommandError("Unable to delete any of specified "
raise exceptions.CommandError("Unable to delete any of the specified "
"cgsnapshots.")
@utils.arg('--detail',
action='store_true',
help='Show detailed information about pools.')
@utils.service_type('volumev2')
def do_get_pools(cs, args):
"""Show pool information for backends. Admin only."""
pools = cs.volumes.get_pools(args.detail)
infos = dict()
infos.update(pools._info)
for info in infos['pools']:
backend = dict()
backend['name'] = info['name']
if args.detail:
backend.update(info['capabilities'])
utils.print_dict(backend)

View File

@@ -58,7 +58,7 @@ class VolumeBackupManager(base.ManagerWithFind):
"""
return self._get("/backups/%s" % backup_id, "backup")
def list(self, detailed=True):
def list(self, detailed=True, search_opts=None):
"""Get a list of all volume backups.
:rtype: list of :class:`VolumeBackup`

View File

@@ -67,7 +67,7 @@ class SnapshotManager(base.ManagerWithFind):
resource_class = Snapshot
def create(self, volume_id, force=False,
name=None, description=None):
name=None, description=None, metadata=None):
"""Creates a snapshot of the given volume.
@@ -76,12 +76,20 @@ class SnapshotManager(base.ManagerWithFind):
attached to an instance. Default is False.
:param name: Name of the snapshot
:param description: Description of the snapshot
:param metadata: Metadata of the snapshot
:rtype: :class:`Snapshot`
"""
if metadata is None:
snapshot_metadata = {}
else:
snapshot_metadata = metadata
body = {'snapshot': {'volume_id': volume_id,
'force': force,
'name': name,
'description': description}}
'description': description,
'metadata': snapshot_metadata}}
return self._create('/snapshots', body, 'snapshot')
def get(self, snapshot_id):

View File

@@ -17,6 +17,11 @@
Volume transfer interface (1.1 extension).
"""
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import six
from cinderclient import base
@@ -69,10 +74,23 @@ class VolumeTransferManager(base.ManagerWithFind):
: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")
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/os-volume-transfer%s%s" % (detail, query_string),
"transfers")
def delete(self, transfer_id):
"""Delete a volume transfer.

View File

@@ -0,0 +1,51 @@
# 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. 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 type access interface."""
from cinderclient import base
class VolumeTypeAccess(base.Resource):
def __repr__(self):
return "<VolumeTypeAccess: %s>" % self.project_id
class VolumeTypeAccessManager(base.ManagerWithFind):
"""
Manage :class:`VolumeTypeAccess` resources.
"""
resource_class = VolumeTypeAccess
def list(self, volume_type):
return self._list(
'/types/%s/os-volume-type-access' % base.getid(volume_type),
'volume_type_access')
def add_project_access(self, volume_type, project):
"""Add a project to the given volume type access list."""
info = {'project': project}
self._action('addProjectAccess', volume_type, info)
def remove_project_access(self, volume_type, project):
"""Remove a project from the given volume type access list."""
info = {'project': project}
self._action('removeProjectAccess', volume_type, info)
def _action(self, action, volume_type, info, **kwargs):
"""Perform a volume type action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/types/%s/action' % base.getid(volume_type)
return self.api.client.post(url, body=body)

View File

@@ -24,6 +24,13 @@ class VolumeType(base.Resource):
def __repr__(self):
return "<VolumeType: %s>" % self.name
@property
def is_public(self):
"""
Provide a user-friendly accessor to os-volume-type-access:is_public
"""
return self._info.get("os-volume-type-access:is_public", 'N/A')
def get_keys(self):
"""Get extra specs from a volume type.
@@ -70,12 +77,15 @@ class VolumeTypeManager(base.ManagerWithFind):
"""Manage :class:`VolumeType` resources."""
resource_class = VolumeType
def list(self, search_opts=None):
def list(self, search_opts=None, is_public=True):
"""Lists all volume types.
:rtype: list of :class:`VolumeType`.
"""
return self._list("/types", "volume_types")
query_string = ''
if not is_public:
query_string = '?is_public=%s' % is_public
return self._list("/types%s" % (query_string), "volume_types")
def get(self, volume_type):
"""Get a specific volume type.
@@ -85,6 +95,13 @@ class VolumeTypeManager(base.ManagerWithFind):
"""
return self._get("/types/%s" % base.getid(volume_type), "volume_type")
def default(self):
"""Get the default volume type.
:rtype: :class:`VolumeType`
"""
return self._get("/types/default", "volume_type")
def delete(self, volume_type):
"""Deletes a specific volume_type.
@@ -92,17 +109,40 @@ class VolumeTypeManager(base.ManagerWithFind):
"""
self._delete("/types/%s" % base.getid(volume_type))
def create(self, name):
def create(self, name, description=None, is_public=True):
"""Creates a volume type.
:param name: Descriptive name of the volume type
:param description: Description of the the volume type
:param is_public: Volume type visibility
:rtype: :class:`VolumeType`
"""
body = {
"volume_type": {
"name": name,
"description": description,
"os-volume-type-access:is_public": is_public,
}
}
return self._create("/types", body, "volume_type")
def update(self, volume_type, name=None, description=None):
"""Update the name and/or description for a volume type.
:param volume_type: The ID of the :class:`VolumeType` to update.
:param name: Descriptive name of the volume type.
:param description: Description of the the volume type.
:rtype: :class:`VolumeType`
"""
body = {
"volume_type": {
"name": name,
"description": description
}
}
return self._update("/types/%s" % base.getid(volume_type),
body, response_key="volume_type")

View File

@@ -24,9 +24,12 @@ except ImportError:
from cinderclient import base
# Valid sort directions and client sort keys
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'size', 'availability_zone', 'name',
'bootable', 'created_at')
# Mapping of client keys to actual sort keys
SORT_KEY_MAPPINGS = {'name': 'display_name'}
class Volume(base.Resource):
@@ -160,6 +163,10 @@ class Volume(base.Resource):
"""Sync the secondary volume with primary for a relationship."""
self.manager.reenable(volume)
def get_pools(self, detail):
"""Show pool information for backends."""
self.manager.get_pools(detail)
class VolumeManager(base.ManagerWithFind):
"""Manage :class:`Volume` resources."""
@@ -226,8 +233,57 @@ class VolumeManager(base.ManagerWithFind):
"""
return self._get("/volumes/%s" % volume_id, "volume")
def _format_sort_param(self, sort):
'''Formats the sort information into the sort query string parameter.
The input sort information can be any of the following:
- Comma-separated string in the form of <key[:dir]>
- List of strings in the form of <key[:dir]>
- List of either string keys, or tuples of (key, dir)
For example, the following import sort values are valid:
- 'key1:dir1,key2,key3:dir3'
- ['key1:dir1', 'key2', 'key3:dir3']
- [('key1', 'dir1'), 'key2', ('key3', dir3')]
:param sort: Input sort information
:returns: Formatted query string parameter or None
:raise ValueError: If an invalid sort direction or invalid sort key is
given
'''
if not sort:
return None
if isinstance(sort, six.string_types):
# Convert the string into a list for consistent validation
sort = [s for s in sort.split(',') if s]
sort_array = []
for sort_item in sort:
if isinstance(sort_item, tuple):
sort_key = sort_item[0]
sort_dir = sort_item[1]
else:
sort_key, _sep, sort_dir = sort_item.partition(':')
sort_key = sort_key.strip()
if sort_key in SORT_KEY_VALUES:
sort_key = SORT_KEY_MAPPINGS.get(sort_key, sort_key)
else:
raise ValueError('sort_key must be one of the following: %s.'
% ', '.join(SORT_KEY_VALUES))
if sort_dir:
sort_dir = sort_dir.strip()
if sort_dir not in SORT_DIR_VALUES:
msg = ('sort_dir must be one of the following: %s.'
% ', '.join(SORT_DIR_VALUES))
raise ValueError(msg)
sort_array.append('%s:%s' % (sort_key, sort_dir))
else:
sort_array.append(sort_key)
return ','.join(sort_array)
def list(self, detailed=True, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, sort=None):
"""Lists all volumes.
:param detailed: Whether to return detailed volume info.
@@ -235,8 +291,10 @@ class VolumeManager(base.ManagerWithFind):
:param marker: Begin returning volumes that appear later in the volume
list than that represented by this volume id.
:param limit: Maximum number of volumes to return.
:param sort_key: Key to be sorted.
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Volume`
"""
if search_opts is None:
@@ -254,19 +312,25 @@ class VolumeManager(base.ManagerWithFind):
if limit:
qparams['limit'] = limit
if sort_key is not None:
if sort_key in SORT_KEY_VALUES:
qparams['sort_key'] = sort_key
else:
raise ValueError('sort_key must be one of the following: %s.'
% ', '.join(SORT_KEY_VALUES))
if sort_dir is not None:
if sort_dir in SORT_DIR_VALUES:
qparams['sort_dir'] = sort_dir
else:
raise ValueError('sort_dir must be one of the following: %s.'
% ', '.join(SORT_DIR_VALUES))
# sort_key and sort_dir deprecated in kilo, prefer sort
if sort:
qparams['sort'] = self._format_sort_param(sort)
else:
if sort_key is not None:
if sort_key in SORT_KEY_VALUES:
qparams['sort_key'] = SORT_KEY_MAPPINGS.get(sort_key,
sort_key)
else:
msg = ('sort_key must be one of the following: %s.'
% ', '.join(SORT_KEY_VALUES))
raise ValueError(msg)
if sort_dir is not None:
if sort_dir in SORT_DIR_VALUES:
qparams['sort_dir'] = sort_dir
else:
msg = ('sort_dir must be one of the following: %s.'
% ', '.join(SORT_DIR_VALUES))
raise ValueError(msg)
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
@@ -281,7 +345,7 @@ class VolumeManager(base.ManagerWithFind):
detail = "/detail"
return self._list("/volumes%s%s" % (detail, query_string),
"volumes")
"volumes", limit=limit)
def delete(self, volume):
"""Delete a volume.
@@ -519,3 +583,11 @@ class VolumeManager(base.ManagerWithFind):
def reenable(self, volume):
"""Sync the secondary volume with primary for a relationship."""
return self._action('os-reenable-replica', volume, None)
def get_pools(self, detail):
"""Show pool information for backends."""
query_string = ""
if detail:
query_string = "?detail=True"
return self._get('/scheduler-stats/get_pools%s' % query_string, None)

View File

@@ -28,7 +28,7 @@ sys.path.insert(0, ROOT)
# Add any Sphinx extension module names here, as strings. They can be
# extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslosphinx']
extensions = ['sphinx.ext.autodoc', 'oslosphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -204,7 +204,3 @@ latex_documents = [
# If false, no module index is generated.
#latex_use_modindex = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

View File

@@ -148,7 +148,7 @@ MASTER
1.0.4
-----
* Added suport for backup-service commands
* Added support 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
@@ -159,7 +159,7 @@ MASTER
-----
* Added support for V2 Cinder API
* Corected upload-volume-to-image help messaging
* Corrected upload-volume-to-image help messaging
* Align handling of metadata args for all methods
* Update OSLO version
* Correct parsing of volume metadata

View File

@@ -1,8 +1,11 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=0.6,!=0.7,<1.0
argparse
PrettyTable>=0.7,<0.8
python-keystoneclient>=0.10.0
requests>=1.2.1
python-keystoneclient>=1.1.0
requests>=2.2.0,!=2.4.0
simplejson>=2.2.0
Babel>=1.3
six>=1.7.0
six>=1.9.0

View File

@@ -1,12 +1,15 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Hacking already pins down pep8, pyflakes and flake8
hacking>=0.8.0,<0.9
coverage>=3.6
discover
fixtures>=0.3.14
mock>=1.0
oslosphinx>=2.2.0.0a2
oslosphinx>=2.5.0 # Apache-2.0
python-subunit>=0.0.18
requests-mock>=0.4.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,<1.3
testtools>=0.9.34
requests-mock>=0.6.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
testtools>=0.9.36,!=1.2.0
testrepository>=0.0.18

View File

@@ -1,15 +1,27 @@
_cinder_opts="" # lazy init
_cinder_flags="" # lazy init
_cinder_opts_exp="" # lazy init
_cinder()
{
local cur prev opts
local cur prev cbc cflags
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="$(cinder bash_completion)"
if [ "x$_cinder_opts" == "x" ] ; then
cbc="`cinder bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`"
_cinder_opts="`echo "$cbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`"
_cinder_flags="`echo " $cbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`"
fi
COMPLETION_CACHE=~/.cinderclient/*/*-cache
opts+=" "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
if [[ "$prev" != "help" ]] ; then
COMPLETION_CACHE=~/.cinderclient/*/*-cache
cflags="$_cinder_flags $_cinder_opts "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
COMPREPLY=($(compgen -W "${cflags}" -- ${cur}))
else
COMPREPLY=($(compgen -W "${_cinder_opts}" -- ${cur}))
fi
return 0
}
complete -F _cinder cinder